Polyline at correct size
Polyline re-rendering too big
Description:
I'm placing a polyline on a MKMap, after zooming in, the panning action will show an oversized pixilated polyline for a period of time till it re-renders it to the correct size.
Solutions I've tried:
Other SOF posts have given solutions to subclass MKPolylineRenderer and set it to a static size. This solution means that the line is either too small to see on large maps or too big for small ones
I've tried making it a solid polyline, if there is any improvement then it's marginal.
I have looked into func applyStrokeProperties(to: CGContext, atZoomScale: MKZoomScale) to resize the whole polyline on each zoom change. Creating the CGContext proved to be above my current skill level and it's unclear if doing this will create performance issues with zooming
Code Samples:
convenience init(polyline: MKPolyline) {
self.init(points: polyline.points(), count: polyline.pointCount)
}
#objc
func renderer() -> MKPolylineRenderer {
let polylineRenderer = MKPolylineRenderer(polyline: self)
polylineRenderer.lineWidth = 5
polylineRenderer.strokeColor = .black
polylineRenderer.lineDashPattern = [0, 10]
return polylineRenderer
}
}```
and implementation:
```- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
// Return a KML renderer if there is one
MKOverlayRenderer *kmlRenderer = [SRMapViewController kmlRendererForKmlOverlay:overlay];
if (kmlRenderer) {
return kmlRenderer;
}
// Return a Route renderer if there is one
if ([overlay isKindOfClass:[RoutePolyline class]]) {
MKPolylineRenderer *renderer = [((RoutePolyline *)overlay) renderer];
return renderer;
}
// Return an MKPolyline if we have a UserLocation path
if ([overlay isKindOfClass:[MKPolyline class]]) {
MKPolylineRenderer *userLocationPathRenderer = [[MKPolylineRenderer alloc] initWithPolyline:overlay];
userLocationPathRenderer.lineWidth = USER_LOCATION_PATH_LINE_WIDTH;
UIColor *userColor = [self colorForUserLocationLine:overlay]; // color for the user of the UserLocation
userLocationPathRenderer.strokeColor = userColor;
return userLocationPathRenderer;
}
// find which area is being rendered and return the renderer for it
MKOverlayRenderer *renderer;
for (NSString *areaId in self.visibleAreaDict) {
MKPolygon *areaOverlay = [self overlayForAreaWithId:areaId];
if (overlay == areaOverlay) {
renderer = [self rendererForAreaWithId:areaId];
break;
}
}
NSAssert(renderer, #"ERROR: self.visibleAreaDict is missing a renderer for overlay %#! It should already be created before this method is called", overlay);
return renderer;
}```
Desired solutions:
- Hopefully an easy fix to speed up the re-render
- A good third party library for the polyline
- A work around to make the problem less visible
Undesired solutions:
- switch to Google Maps
Thanks!!
After a bunch of research it turned out that you can't speed it up. Apple maps uses it's own custom renderer which is far more performant. For the rest of us, we just need to work with the lag.
Related
I have a UITableView that I am attempting to expand and collapse using its beginUpdates and endUpdates methods and have a drop shadow displayed as that's happening. In my custom UITableViewCell, I have a layer which I create a shadow for in layoutSubviews:
self.shadowLayer.shadowColor = self.shadowColor.CGColor;
self.shadowLayer.shadowOffset = CGSizeMake(self.shadowOffsetWidth, self.shadowOffsetHeight);
self.shadowLayer.shadowOpacity = self.shadowOpacity;
self.shadowLayer.masksToBounds = NO;
self.shadowLayer.frame = self.layer.bounds;
// this is extremely important for performance when drawing shadows
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:self.shadowLayer.frame cornerRadius:self.cornerRadius];
self.shadowLayer.shadowPath = shadowPath.CGPath;
I add this layer to the UITableViewCell in viewDidLoad:
self.shadowLayer = [CALayer layer];
self.shadowLayer.backgroundColor = [UIColor whiteColor].CGColor;
[self.layer insertSublayer:self.shadowLayer below:self.contentView.layer];
As I understand it, when I call beginUpdates, an implicit CALayerTransaction is made for the current run loop if none exists. Additionally, layoutSubviews also gets called. The problem here is that the resulting shadow is drawn immediately based on the new size of the UITableViewCell. I really need to shadow to continue to cast in the expected way as the actual layer is animating.
Since my created layer is not a backing CALayer it animates without explicitly specifying a CATransaction, which is expected. But, as I understand it, I really need some way to grab hold of beginUpdates/endUpdates CATransaction and perform the animation in that. How do I do that, if at all?
So I guess you have something like this:
(I turned on “Debug > Slow Animations” in the simulator.) And you don't like the way the shadow jumps to its new size. You want this instead:
You can find my test project in this github repository.
See #horseshoe7's answer for a Swift translation.
It is tricky but not impossible to pick up the animation parameters and add an animation in the table view's animation block. The trickiest part is that you need to update the shadowPath in the layoutSubviews method of the shadowed view itself, or of the shadowed view's immediate superview. In my demo video, that means that the shadowPath needs to be updated by the layoutSubviews method of the green box view or the green box's immediate superview.
I chose to create a ShadowingView class whose only job is to draw and animate the shadow of one of its subviews. Here's the interface:
#interface ShadowingView : UIView
#property (nonatomic, strong) IBOutlet UIView *shadowedView;
#end
To use ShadowingView, I added it to my cell view in my storyboard. Actually it's nested inside a stack view inside the cell. Then I added the green box as a subview of the ShadowingView and connected the shadowedView outlet to the green box.
The ShadowingView implementation has three parts. One is its layoutSubviews method, which sets up the layer shadow properties on its own layer to draw a shadow around its shadowedView subview:
#implementation ShadowingView
- (void)layoutSubviews {
[super layoutSubviews];
CALayer *layer = self.layer;
layer.backgroundColor = nil;
CALayer *shadowedLayer = self.shadowedView.layer;
if (shadowedLayer == nil) {
layer.shadowColor = nil;
return;
}
NSAssert(shadowedLayer.superlayer == layer, #"shadowedView must be my direct subview");
layer.shadowColor = UIColor.blackColor.CGColor;
layer.shadowOffset = CGSizeMake(0, 1);
layer.shadowOpacity = 0.5;
layer.shadowRadius = 3;
layer.masksToBounds = NO;
CGFloat radius = shadowedLayer.cornerRadius;
layer.shadowPath = CGPathCreateWithRoundedRect(shadowedLayer.frame, radius, radius, nil);
}
When this method is run inside an animation block (as is the case when the table view animates a change in the size of a cell), and the method sets shadowPath, Core Animation looks for an “action” to run after updating shadowPath. One of the ways it looks is by sending actionForLayer:forKey: to the layer's delegate, and the delegate is the ShadowingView. So we override actionForLayer:forKey: to provide an action if possible and appropriate. If we can't, we just call super.
It is important to understand that Core Animation asks for the action from inside the shadowPath setter, before actually changing the value of shadowPath.
To provide the action, we make sure the key is #"shadowPath", that there is an existing value for shadowPath, and that there is already an animation on the layer for bounds.size. Why do we look for an existing bounds.size animation? Because that existing animation has the duration and timing function we should use to animate shadowPath. If everything is in order, we grab the existing shadowPath, make a copy of the animation, store them in an action, and return the action:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (![event isEqualToString:#"shadowPath"]) { return [super actionForLayer:layer forKey:event]; }
CGPathRef priorPath = layer.shadowPath;
if (priorPath == NULL) { return [super actionForLayer:layer forKey:event]; }
CAAnimation *sizeAnimation = [layer animationForKey:#"bounds.size"];
if (![sizeAnimation isKindOfClass:[CABasicAnimation class]]) { return [super actionForLayer:layer forKey:event]; }
CABasicAnimation *animation = [sizeAnimation copy];
animation.keyPath = #"shadowPath";
ShadowingViewAction *action = [[ShadowingViewAction alloc] init];
action.priorPath = priorPath;
action.pendingAnimation = animation;
return action;
}
#end
What does the action look like? Here's the interface:
#interface ShadowingViewAction : NSObject <CAAction>
#property (nonatomic, strong) CABasicAnimation *pendingAnimation;
#property (nonatomic) CGPathRef priorPath;
#end
The implementation requires a runActionForKey:object:arguments: method. In this method, we update the animation that we created in actionForLayer:forKey: using the saved-away old shadowPath and the new shadowPath, and then we add the animation to the layer.
We also need to manage the retain count of the saved path, because ARC doesn't manage CGPath objects.
#implementation ShadowingViewAction
- (void)runActionForKey:(NSString *)event object:(id)anObject arguments:(NSDictionary *)dict {
if (![anObject isKindOfClass:[CALayer class]] || _pendingAnimation == nil) { return; }
CALayer *layer = anObject;
_pendingAnimation.fromValue = (__bridge id)_priorPath;
_pendingAnimation.toValue = (__bridge id)layer.shadowPath;
[layer addAnimation:_pendingAnimation forKey:#"shadowPath"];
}
- (void)setPriorPath:(CGPathRef)priorPath {
CGPathRetain(priorPath);
CGPathRelease(_priorPath);
_priorPath = priorPath;
}
- (void)dealloc {
CGPathRelease(_priorPath);
}
#end
This is Rob Mayoff's answer written in Swift. Could save someone some time.
Please don't upvote this. Upvote Rob Mayoff's solution. It is awesome, and correct. (Note from mayoff: why not upvote both? 😉)
import UIKit
class AnimatingShadowView: UIView {
struct DropShadowParameters {
var shadowOpacity: Float = 0
var shadowColor: UIColor? = .black
var shadowRadius: CGFloat = 0
var shadowOffset: CGSize = .zero
static let defaultParameters = DropShadowParameters(shadowOpacity: 0.15,
shadowColor: .black,
shadowRadius: 5,
shadowOffset: CGSize(width: 0, height: 1))
}
#IBOutlet weak var contentView: UIView! // no sense in have a shadowView without content!
var shadowParameters: DropShadowParameters = DropShadowParameters.defaultParameters
private func apply(dropShadow: DropShadowParameters) {
let layer = self.layer
layer.shadowColor = dropShadow.shadowColor?.cgColor
layer.shadowOffset = dropShadow.shadowOffset
layer.shadowOpacity = dropShadow.shadowOpacity
layer.shadowRadius = dropShadow.shadowRadius
layer.masksToBounds = false
}
override func layoutSubviews() {
super.layoutSubviews()
let layer = self.layer
layer.backgroundColor = nil
let contentLayer = self.contentView.layer
assert(contentLayer.superlayer == layer, "contentView must be a direct subview of AnimatingShadowView!")
self.apply(dropShadow: self.shadowParameters)
let radius = contentLayer.cornerRadius
layer.shadowPath = UIBezierPath(roundedRect: contentLayer.frame, cornerRadius: radius).cgPath
}
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
guard event == "shadowPath" else {
return super.action(for: layer, forKey: event)
}
guard let priorPath = layer.shadowPath else {
return super.action(for: layer, forKey: event)
}
guard let sizeAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation else {
return super.action(for: layer, forKey: event)
}
let animation = sizeAnimation.copy() as! CABasicAnimation
animation.keyPath = "shadowPath"
let action = ShadowingViewAction()
action.priorPath = priorPath
action.pendingAnimation = animation
return action
}
}
private class ShadowingViewAction: NSObject, CAAction {
var pendingAnimation: CABasicAnimation? = nil
var priorPath: CGPath? = nil
// CAAction Protocol
func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
guard let layer = anObject as? CALayer, let animation = self.pendingAnimation else {
return
}
animation.fromValue = self.priorPath
animation.toValue = layer.shadowPath
layer.add(animation, forKey: "shadowPath")
}
}
Assuming that you're manually setting your shadowPath, here's a solution inspired by the others here that accomplishes the same thing using less code.
Note that I'm intentionally constructing my own CABasicAnimation rather than copying the bounds.size animation exactly, as in my own tests I found that toggling the copied animation while it was still in progress could cause the animation to snap to it's toValue, rather than transitioning smoothly from its current value.
class ViewWithAutosizedShadowPath: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let oldShadowPath = layer.shadowPath
let newShadowPath = CGPath(rect: bounds, transform: nil)
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let shadowPathAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowPathAnimation.duration = boundsAnimation.duration
shadowPathAnimation.timingFunction = boundsAnimation.timingFunction
shadowPathAnimation.fromValue = oldShadowPath
shadowPathAnimation.toValue = newShadowPath
layer.add(shadowPathAnimation, forKey: "shadowPath")
}
layer.shadowPath = newShadowPath
}
}
UITableView is likely not creating a CATransaction, or if it is, it's waiting until after you end the updates. My understanding is that table views just coalesce all changes between those functions and then creates the animations as necessary. You don't have a way to get a handle on the actual animation parameters it's committing, because we don't know when that actually happens. The same thing happens when you animate a content offset change in UIScrollView: the system provides no context about the animation itself, which is frustrating. There is also no way to query the system for current CATransactions.
Probably the best you can do is inspect the animation that UITableView is creating and just mimic the same timing parameters in your own animation. Swizzling add(_:forKey:) on CALayer can allow you to inspect all animations being added. You certainly don't want to actually ship with this, but I often use this technique in debugging to figure out what animations are being added and what their properties are.
I suspect that you're going to have to commit your own shadow layer animations in tableView(_:willDisplayCell:for:row:) for the appropriate cells.
I have a MKMapView working with a MKTileOverlay showing tiles from a local database. It's working fine.
Then I used MKDirections to get direction between two coordinates and draw the route like that :
MKRoute *route = response.routes.lastObject;
MKPolyline *polyline = route.polyline;
// Draw path on overlay
[self.mapView insertOverlay:polyline aboveOverlay:self.tileOverlay];
But when I zoom to see the line, it appears without the tile background (normaly loaded from MKTileOverlay (stored into self.tileOverlay)). I joined an image to see better.
I also made this code to render overlays :
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
if ([overlay isKindOfClass:[MKTileOverlay class]]) {
return [[MKTileOverlayRenderer alloc] initWithTileOverlay:overlay];
}
else if ([overlay isKindOfClass:[MKPolyline class]]) {
MKPolylineRenderer *lineView = [[MKPolylineRenderer alloc] initWithOverlay:overlay];
lineView.strokeColor = [UIColor greenColor];
lineView.lineWidth = 3;
return lineView;
}
return nil;
}
It's like the "tile" that render the line hide the tile loaded from the MKTileOverlay. How can I :
- specify that I the MKPolyline overlay must be transparent ?
- reload the background tile ?
Screeshot :
See the tile with line has no background anymore http://sigmanet.ch/tmp/screen.png
After days of work, here is my own solution.
Extend MKPolylineRenderer and add a reference to the MKTileOverlayRenderer. Let's call this new class MCPolylineRenderer.
In this class, override this two methods :
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context {
// Draw path only if tile render has a tile
if ([self.tileRenderRef canDrawMapRect:mapRect zoomScale:zoomScale]) {
[super drawMapRect:mapRect zoomScale:zoomScale inContext:context];
}
}
- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {
// We can draw path only if tile render can also
return [self.tileRenderRef canDrawMapRect:mapRect zoomScale:zoomScale];
}
Now, in the mapView:renderedForOverlay method, replace
MKPolylineRenderer *lineView = [[MKPolylineRenderer alloc] initWithOverlay:overlay];
with
MCPolylineRenderer *lineView = [[MCPolylineRenderer alloc] initWithPolyline:overlay];
lineView.tileRenderRef = self.tileRender;
Also, you need to be sure that the loadTileAtPath:result: method doesn't result a tile when there is nothing to render (like a "tile not found" image).
This code will have effect that when there is no background tile to render, the path won't be draw neither.
You'll have to subclass MKPolylineRenderer to synchronize renderers drawing abilities.
import Foundation
import MapKit
class MyPolylineRenderer: MKPolylineRenderer {
var tileRenderer: MKTileOverlayRenderer?
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
if (tileRenderer?.canDraw(mapRect, zoomScale: zoomScale) ?? true) {
super.draw(mapRect, zoomScale: zoomScale, in: context)
}
}
override func canDraw(_ mapRect: MKMapRect, zoomScale: MKZoomScale) -> Bool {
return tileRenderer?.canDraw(mapRect, zoomScale: zoomScale) ?? super.canDraw(mapRect, zoomScale: zoomScale)
}
}
Then in your MKMapViewDelegate, keep a reference to your tileRenderer and implement the rendererForOverlay :
var tileRenderer: MKTileOverlayRenderer?
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polyline = overlay as? MKPolyline {
let lineRenderer = NXPolylineRenderer(overlay: overlay)
lineRenderer.tileRenderer = tileRenderer
// Configure your polyline overlay renderer here...
return lineRenderer;
}
if let tileOverlay = overlay as? MKTileOverlay {
if tileRenderer == nil || tileRenderer.overlay != overlay {
tileRenderer = MKTileOverlayRenderer(overlay: overlay)
}
return tileRenderer
}
return MKOverlayRenderer(overlay: overlay)
}
All credits for the idea goes to #Jonathan, I'm just posting a swift ready to copy/paste code for newcomers.
I have a map view that has annotations on it. With each annotation, I need to show an overlay that is just a circle. This circle needs to be one of two different colours depending on a value. The user can select something else on the screen, and all annotations and overlays need to be removed, and new ones need to be added based on the newly selected item.
The first time I load the controller having the map view, everything works great. When the user selects a new item, the old annotations and overlays are removed, and the new annotations and overlays are added, but the overlays don't appear. When I put breakpoints in, I don't see the mapview:rendererForOverlay method getting called after a user has selected a new item. Below is the code I am using:
In viewDidLoad, I have the following:
_mapview.delegate = self;
[self placePinsOnMap];
The placePinsOnMap: method has the following:
- (void)placePinsOnMap {
AMapAnnotation* annotation;
for (APlace* place in _selectedItem.places) {
annotation = [[AMapAnnotation alloc] initWithTitle:place.name subtitle:place.subtitle coordinate:place.location];
annotation.object = place;
[_mapview addAnnotation:annotation];
[_mapview addOverlay:[[AMapOverlay alloc] initWithCenterCoordinate:place.location radius:[_place.size floatValue]*1000 object:place] level:MKOverlayLevelAboveRoads];
}
}
Each time an overlay is added, the mapview:rendererForOverlay is called. This looks like this:
- (MKOverlayRenderer*)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
MKCircleRenderer* circleRenderer;
AMapOverlay* mapOverlay;
APlace* place;
mapOverlay = (AMapOverlay*)overlay;
place = mapOverlay.object;
circleRenderer = [[MKCircleRenderer alloc] initWithCircle:mapOverlay];
if ([place.radius floatValue] >= 5 && [place.radius floatValue] <= 10) {
circleRenderer.fillColor = [[UIColor greenColor] colorWithAlphaComponent:0.2];
} else {
circleRenderer.fillColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
}
return circleRenderer;
}
When a user selects a new item, I remove all annotations and overlays, and I place the new annotations and their overlays for the new item selected:
- (void)onScrollviewTap:(UITapGestureRecognizer*)recognizer {
CGPoint point;
point = [recognizer locationInView:_scrollview];
for (ACard* card in _cards) {
if (CGRectContainsPoint(card.frame, point)) {
card.selected = YES;
_selectedItem = card.item;
NSArray* array = [_mapview annotations];
for (id<MKAnnotation> annotation in array) {
if (annotation != _userAnnotation) {
[_mapview removeAnnotation:annotation];
}
}
[_mapview removeOverlays:[_mapview overlays]];
[self placePinsOnMap];
} else {
card.selected = NO;
}
}
}
The AMapOverlay class is a subclass of MKCircle and simply holds the coordinate, a radius value and a place object which is used to determine what colour the overlay should be. I have the boundingMapRect overridden in the AMapOverlay class:
- (MKMapRect)boundingMapRect {
MKMapPoint upperLeft = MKMapPointForCoordinate(self.coordinate);
MKMapRect bounds = MKMapRectMake(upperLeft.x, upperLeft.y, self.radius*2, self.radius*2);
return bounds;
}
Just to reiterate what my problem is: The first time I load the controller having the map view, everything works great. I see the annotations along with their overlays. When the user selects a new item, the old annotations and overlays are removed, and the new annotations are added along with the overlays, but the overlays don't appear.
Does anyone have any ideas why this would be?
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")
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]]; });