MKAnnotationView Doesn't Stay In Place When Zooming - objective-c

I'm placing custom markers on my map in iOS and I'm having a problem whereby when the user pinches to zoom in and out, the markers don't anchor to where they should. Here's the code that adds markers...
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil;
MarkerVO *thisMarker = (MarkerVO*)annotation;
MKAnnotationView *pin = (MKAnnotationView *) [map dequeueReusableAnnotationViewWithIdentifier:[thisMarker commaSeparatedCoordinate]];
if (!pin) {
pin = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:[thisMarker commaSeparatedCoordinate]] autorelease];
[pin setImage:[UIImage imageNamed:#"pin_tick.png"]];
[pin setCenterOffset:CGPointMake(0, -23)];
[pin setCanShowCallout:YES];
}
return pin;
}
So yes, the tick marker displays ok but on zoom, it just moves around. For example, it could be right on the spot at close zoom but zooming way out ends up with it being in the sea! I get the idea of why this is happening however even without the setCenterOffset line, it's still happening.
Any ideas would be great.

When pin does not return nil from the dequeue, try setting pin.annotation to annotation.
The re-used view might somehow be from a different annotation even though the code seems to be setting a unique identifier for each annotation (which I don't recommend in any case).

Related

MKMapView Overlay (image animation)

I'm trying to animate a radar image on an MKMapView. I have separate images for different times, and I have successfully set an image as an overlay on top of the MKMapView. My problem is when I try to show these different images in a sequence one after each other. I tried a method of adding and removing overlays, but the system does not handle it well at all, with flickering of overlays, and some overlays not being removed, and overall, its not worth it.
Could anyone help me find a way to show multiple images (like an animated gif) on top of a MapView in a way that is smooth and fast?
Here's my code to overlay the image:
- (id)initWithImageData:(NSData*)imageData withLowerLeftCoordinate:(CLLocationCoordinate2D)lowerLeftCoordinate withUpperRightCoordinate:(CLLocationCoordinate2D)upperRightCoordinate {
self.radarData = imageData;
MKMapPoint lowerLeft = MKMapPointForCoordinate(lowerLeftCoordinate);
MKMapPoint upperRight = MKMapPointForCoordinate(upperRightCoordinate);
mapRect = MKMapRectMake(lowerLeft.x, upperRight.y, upperRight.x - lowerLeft.x, lowerLeft.y - upperRight.y);
return self;
}
- (CLLocationCoordinate2D)coordinate
{
return MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMidX(mapRect), MKMapRectGetMidY(mapRect)));
}
- (MKMapRect)boundingMapRect
{
return mapRect;
}
and here's how I'm setting it up on the MapView:
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)ctx
{
MapOverlay *mapOverlay = (MapOverlay *)self.overlay;
image = [[UIImage alloc] initWithData:mapOverlay.radarData];
MKMapRect theMapRect = [self.overlay boundingMapRect];
CGRect theRect = [self rectForMapRect:theMapRect];
#try {
UIGraphicsBeginImageContext(image.size);
UIGraphicsPushContext(ctx);
[image drawInRect:theRect blendMode:kCGBlendModeNormal alpha:0.5];
UIGraphicsPopContext();
UIGraphicsEndImageContext();
}
#catch (NSException *exception) {
NSLog(#"Caught an exception while drawing radar on map - %#",[exception description]);
}
#finally {
}
}
and here's where I'm adding the overlay:
- (void)mapRadar {
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
self.mapOverlay = [[MapOverlay alloc] initWithImageData:appDelegate.image withLowerLeftCoordinate:CLLocationCoordinate2DMake(appDelegate.south, appDelegate.west) withUpperRightCoordinate:CLLocationCoordinate2DMake(appDelegate.north, appDelegate.east)];
[self.mapView addOverlay:self.mapOverlay];
[self.mapView setNeedsDisplay];
self.mapView.showsUserLocation = YES;
MKMapPoint lowerLeft2 = MKMapPointForCoordinate(CLLocationCoordinate2DMake(appDelegate.south2, appDelegate.west2) );
MKMapPoint upperRight2 = MKMapPointForCoordinate(CLLocationCoordinate2DMake(appDelegate.north2, appDelegate.east2));
MKMapRect localMapRect = MKMapRectMake(lowerLeft2.x, upperRight2.y, upperRight2.x - lowerLeft2.x, lowerLeft2.y - upperRight2.y);
[self.mapView setVisibleMapRect:localMapRect animated:YES];
}
#pragma Mark - MKOverlayDelgateMethods
-(MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id)overlay{
if([overlay isKindOfClass:[MapOverlay class]]) {
MapOverlay *mapOverlay = overlay;
self.mapOverlayView = [[MapOverlayView alloc] initWithOverlay:mapOverlay];
}
return self.mapOverlayView;
}
Does anyone have an idea or a solution where I can show a sequence of images on an MKMapView smoothly? At this point, I'm desperate for solutions and can't find it anywhere else, so anything would help me, thanks!
Using Lefteris' suggestion, you can just use UIImageView with animationImages. The animation should be smooth provided your overlay images are properly sized for the screen.
To pass touches through to the map view below, you can create a new UIView subclass with a (weak) reference to the map view. Override the touch event methods (touchesBegan:withEvent:, touchesMoved:withEvent:, etc) to forward to the map view, like so:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.mapView touchesBegan:touches withEvent:event];
}
This way the user can still interact with the view below.
You may run into problems when the user tries to zoom, as it will be difficult if not impossible to scale all your animation frames in real-time. You should look into using CATiledLayer instead.

MKMapView and annotations hiding with zoom

I am using MKMapView... I am adding tousands of annotations to map, which causes slow map movements.
I want to show / hide annotations with zoom level. In each zoom I want to hide overlapping annotations.
Is there any solution ?
So far I came up with comapring annotation bounding rectangles on overlap and remove annotation, if there is an overlap. This solution is slow, because i need to compare everything with everything (I know, I can use trees etc... ) and secondly, removing and adding annotations back to map is little slow.
What would be good, is to have access to annotation rendering and if annotation is rendered, check whether it can be or not...
Can it be done ?
Thanks
You can use the following code
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
NSArray *annotations = [mapView annotations];
//NSLog(#"%#",annotations);
CustomAnnotation *annotation = nil;
for (int i=0; i<[annotations count]; i++) {
annotation = (CustomAnnotation*)[annotations objectAtIndex:i];
if (![annotation isKindOfClass:[MKUserLocation class]]) {
if (mapView.region.span.latitudeDelta <= 0.13f) {
[[mapView viewForAnnotation:annotation] setHidden:NO];
} else {
[[mapView viewForAnnotation:annotation] setHidden:YES];
}
}
}
}
You can adjust the delta in the if condition for more comfortable
You can achieve this using the native SDK since iOS 11, check the documentation (so-called "MapKit Annotation Clustering")

MKPinAnnotationView custom Image is replaced by pin with animating drop

I'm displaying user avatar images on a MapView. The avatars appear to be working if they are rendered without animation, however I'd like to animate their drop. If I set pinView.animatesDrop to true, then I lose the avatar and it is replaced by the pin. How can I animate the drop of my avatar without using the pin?
Here is my viewForAnnotation method I'm using:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil;
MKPinAnnotationView* pinView = (MKPinAnnotationView*)[self.mapView dequeueReusableAnnotationViewWithIdentifier:#"MyAnnotation"];
if (!pinView) {
pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"CustomPinAnnotation"];
}
else
pinView.annotation = annotation;
//If I set this to true, my image is replaced by the pin
//pinView.animatesDrop = true;
//I'm using SDWebImage
[pinView setImageWithURL:[NSURL URLWithString:#"/pathtosomeavatar.png"] placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
//Adding a nice drop shadow here
pinView.layer.shadowColor = [UIColor blackColor].CGColor;
pinView.layer.shadowOffset = CGSizeMake(0, 1);
pinView.layer.shadowOpacity = 0.5;
pinView.layer.shadowRadius = 3.0;
return pinView;
}
When you want to use your own image for an annotation view, it's best to create a regular MKAnnotationView instead of an MKPinAnnotationView.
Since MKPinAnnotationView is a subclass of MKAnnotationView, it has an image property but it generally (usually when you don't expect it) ignores it and displays its built-in pin image instead.
So rather than fighting with it, it's best to just use a plain MKAnnotationView.
The big thing you lose by not using MKPinAnnotationView is the built-in drop animation which you'll have to implement manually.
See this previous answer for more details:
MKMapView: Instead of Annotation Pin, a custom view
If you're stumbling on to this issue and you're sure that you're MKAnnotationView instead of an MKPinAnnotationView, ensure your MKMapView's delegate property hasn't been nilled.

Z-index of iOS MapKit user location annotation

I need to draw the current user annotation (the blue dot) on top of all other annotations. Right now it is getting drawn underneath my other annotations and getting hidden. I'd like to adjust the z-index of this annotation view (the blue dot) and bring it to the top, does anyone know how?
Update:
So I can now get a handle on the MKUserLocationView, but how do I bring it forward?
- (void) mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views {
for (MKAnnotationView *view in views) {
if ([[view annotation] isKindOfClass:[MKUserLocation class]]) {
// How do I bring this view to the front of all other annotations?
// [???? bringSubviewToFront:????];
}
}
}
Finally got it to work using the code listed below thanks to the help from Paul Tiarks. The problem I ran into is that the MKUserLocation annotation gets added to the map first before any others, so when you add the other annotations their order appears to be random and would still end up on top of the MKUserLocation annotation. To fix this I had to move all the other annotations to the back as well as move the MKUserLocation annotation to the front.
- (void) mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views
{
for (MKAnnotationView *view in views)
{
if ([[view annotation] isKindOfClass:[MKUserLocation class]])
{
[[view superview] bringSubviewToFront:view];
}
else
{
[[view superview] sendSubviewToBack:view];
}
}
}
Update: You may want to add the code below to ensure the blue dot is drawn on top when scrolling it off the viewable area of the map.
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
for (NSObject *annotation in [mapView annotations])
{
if ([annotation isKindOfClass:[MKUserLocation class]])
{
NSLog(#"Bring blue location dot to front");
MKAnnotationView *view = [mapView viewForAnnotation:(MKUserLocation *)annotation];
[[view superview] bringSubviewToFront:view];
}
}
}
Another solution:
setup annotation view layer's zPosition (annotationView.layer.zPosition) in:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views;
The official answer to that thread is wrong... using zPosition is indeed the best approach and fastest vs using regionDidChangeAnimated...
else you would suffer big performance impact with many annotations on map (as every change of frame would rescan all annotations). and been testing it...
so when creating the view of the annotation (or in didAddAnnotationViews) set :
self.layer.zPosition = -1; (below all others)
and as pointed out by yuf:
This makes the pin cover callouts from other pins – yuf Dec 5 '13 at 20:25
i.e. the annotation view will appear below other pins.
to fix, simply reput the zPosition to 0 when you have a selection
-(void) mapView:(MKMapView*)mapView didSelectAnnotationView:(MKAnnotationView*)view {
if ([view isKindOfClass:[MyCustomAnnotationView class]])
view.layer.zPosition = 0;
...
}
-(void) mapView:(MKMapView*)mapView didDeselectAnnotationView:(MKAnnotationView*)view {
if ([view isKindOfClass:[MyCustomAnnotationView class]])
view.layer.zPosition = -1;
...
}
Update for iOS 14
I know it's an old post, but the question is still applicable and you end up here when typing it into your favorite search engine.
Starting with iOS 14, Apple introduced a zPriority property to MKAnnotationView. You can use it to set up the z-index for your annotations using predefined constants or floats.
Also, Apple made it possible to finally create the view for the user location on our own and provided MKUserLocationView as a subclass of MKAnnotationView.
From the documentation for MKUserLocationView:
If you want to specify additional configuration, such as zPriority,
create this annotation view directly. To display the annotation view,
return the instance from mapView(_:viewFor:).
The following code snippet shows how this can be done (add the code to your MKMapViewDelegate):
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// Alter the MKUserLocationView (iOS 14+)
if #available(iOS 14.0, *), annotation is MKUserLocation {
// Try to reuse the existing view that we create below
let reuseIdentifier = "userLocation"
if let existingView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) {
return existingView
}
let view = MKUserLocationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
view.zPriority = .max // Show user location above other annotations
view.isEnabled = false // Ignore touch events and do not show callout
return view
}
// Create views for other annotations or return nil to use the default representation
return nil
}
Note that per default, the user location annotation shows a callout when tapping on it. Now that the user location overlays your other annotations, you'd probably want to disable this, which is done in the code by setting .isEnabled to false.
Just use the .layer.anchorPointZ property.
Example:
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
views.forEach {
if let _ = $0.annotation as? MKUserLocation {
$0.layer.anchorPointZ = 0
} else {
$0.layer.anchorPointZ = 1
}
}
}
Here is there reference https://developer.apple.com/documentation/quartzcore/calayer/1410796-anchorpointz
Try, getting a reference to the user location annotation (perhaps in mapView: didAddAnnotationViews:) and then bring that view to the front of the mapView after all of your annotations have been added.
Swift 3:
internal func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
for annotationView in views {
if annotationView.annotation is MKUserLocation {
annotationView.bringSubview(toFront: view)
return
}
annotationView.sendSubview(toBack: view)
}
}
Here is a way to do it using predicates. I think it should be faster
NSPredicate *userLocationPredicate = [NSPredicate predicateWithFormat:#"class == %#", [MKUserLocation class]];
NSArray* userLocation = [[self.mapView annotations] filteredArrayUsingPredicate:userLocationPredicate];
if([userLocation count]) {
NSLog(#"Bring blue location dot to front");
MKAnnotationView *view = [self.mapView viewForAnnotation:(MKUserLocation *)[userLocation objectAtIndex:0]];
[[view superview] bringSubviewToFront:view];
}
Using Underscore.m
_.array(mapView.annotations).
filter(^ BOOL (id<MKAnnotation> annotation) { return [annotation
isKindOfClass:[MKUserLocation class]]; })
.each(^ (id<MKAnnotation> annotation) { [[[mapView
viewForAnnotation:annotation] superview] bringSubviewToFront:[mapView
viewForAnnotation:annotation]]; });

NSOutlineView Drag-n-Drop

In my application, NSOutlineView used as below,
1 -- Using CustomOutlineView because i want to control NSOutlineView Background,
2 -- Using CustomeCell, becuase i need to have customize Cell
Header File : MyListView.h
/*
MyUICustomView is the Subclass from NSView and this is i need to have
for some other my application purpose
*/
#interface MyListView : MyUICustomView<NSOutlineViewDataSource>
{
// MyCustomOutlineview because, i need to override DrawRect Method to have
// customized background
MyCustomOutlineView *pMyOutlineView;
}
#property(nonatomic,retain)MyCustomOutlineView *pMyOutlineView;
and also i should be able to drag-n-drop within Outline view,
for having drag-n-drop i have done following,
-(void)InitOutlineView{
// Creating outline view
NSRect scrollFrame = [self bounds];
NSScrollView* scrollView = [[[NSScrollView alloc] initWithFrame:scrollFrame] autorelease];
[scrollView setBorderType:NSNoBorder];
[scrollView setHasVerticalScroller:YES];
[scrollView setHasHorizontalScroller:NO];
[scrollView setAutohidesScrollers:YES];
[scrollView setDrawsBackground: NO];
NSRect clipViewBounds = [[scrollView contentView] bounds];
pMyOutlineView = [[[MyCustomOutlineView alloc] initWithFrame:clipViewBounds] autorelease];
NSTableColumn* firstColumn = [[[NSTableColumn alloc] initWithIdentifier:#"firstColumn"] autorelease];
#ifdef ENABLE_CUSTOM_CELL
// Becuase cell should have Image, Header Info and brief detail in small font,
// so i need to have custom cell
ImageTextCell *pCell = [[ImageTextCell alloc]init];
[firstColumn setDataCell:pCell];
// SO i can fill the data
[pCell setDataDelegate:self];
# endif
[pMyOutlineView setDataSource:self];
/* This is to tell MyCustomOutlineView to handle the context menu */
[pMyOutlineView setDataDelegate:self];
[scrollView setDocumentView:pCTOutlineView];
[pMyOutlineView addTableColumn:firstColumn];
[pMyOutlineView registerForDraggedTypes:
[NSArray arrayWithObjects:OutlinePrivateTableViewDataType,nil]];
[pMyOutlineView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES];**
}
and to Support drag-n-drop Implemented following method
- (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard{
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:items];
[pboard declareTypes:[NSArray arrayWithObject:OutlinePrivateTableViewDataType] owner:self];
[pboard setData:data forType:OutlinePrivateTableViewDataType];
return YES;
}
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index{
// Add code here to validate the drop
NSLog(#"validate Drop");
return NSDragOperationEvery;
}
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id < NSDraggingInfo >)info item:(id)item childIndex:(NSInteger)index{
NSLog(#"validate Drop");
}
but still when i try to drag the row of NSOutlineView nothing is happening, even i tried to debug via NSLog but i couldn't see any log from above function,
Am i missing any important method ?
to support Drag-n-drop
Based on your code, it looks like you're not returning anything for the ...writeItems... method. This method is supposed to return a BOOL (whether the items were successfully written or you're denying the drag). Returning nothing should be giving you a compiler warning ...
HI,
FInally got able to rid of this,
The problem is
[firstColumn setWidth:25];
So drag was working only upto 25 pixel form the left, and i commented this, then its working time, but strange to see, my outline view has only one column , for drawing, its not checking the restriction but for drag-n-drop its checking,
Thanks Joshua