MKMapView and annotations hiding with zoom - objective-c

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")

Related

How do I speed up polyline re-rendering when panning with MKMap?

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.

UISegmentedControl with Bezeled Style uncenters titles on Device

for clarification I'll just add 2 overlaid Screenshots, one in Interface Builder, the other on the device.
The lower UISegmentedControl is fresh out of the library with no properties edited, still it looks different on the Device (in this case a non-Retina iPad, though the problem is the same for Retina-iPhone) (Sorry for the quick and dirty photoshopping)
Any ideas?
EDIT: I obviously tried the "alignment" under "Control" in the Utilities-Tab in Interface Builder. Unfortunately none of the settings changed anything for the titles in the UISegment. I don't think they should as they are not changing titles in Interface Builder either.
EDIT2: Programmatically setting:
eyeSeg.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
doesn't make a difference either.
Found the Problem "UISegmentedControlStyleBezeled is deprecated. Please use a different style."
See also what-should-i-use-instead-of-the-deprecated-uisegmentedcontrolstylebezeled-in-io
Hmm...have you checked the alignment? Maybe that's the case.
You can recursively search the subviews of the UISegmentedControl view for each of the UILabels in the segmented control and then change the properties of each UILabel including the textAlignment property as I've shown in a sample of my code. Credit to Primc's post in response to Change font size of UISegmented Control for suggesting this general approach to customizing the UILabels of a UISegmentedControl. I had been using this code with the UISegmentedControlStyleBezeled style by the way even after it was deprecated although I have recently switched to UISegmentedControlStyleBar with an adjusted frame height.
- (void)viewDidLoad {
[super viewDidLoad];
// Adjust the segment widths to fit the text. (Will need to calculate widths if localized text is ever used.)
[aspirationControl setWidth:66 forSegmentAtIndex:0]; // Navel Lint Collector
[aspirationControl setWidth:48 forSegmentAtIndex:1]; // Deep Thinker
[aspirationControl setWidth:49 forSegmentAtIndex:2]; // Mental Wizard
[aspirationControl setWidth:64 forSegmentAtIndex:3]; // Brilliant Professor
[aspirationControl setWidth:58 forSegmentAtIndex:4]; // Nobel Laureate
// Reduce the font size of the segmented aspiration control
[self adjustSegmentText:aspirationControl];
}
- (void)adjustSegmentText:(UIView*)view {
// A recursively called method for finding the subviews containing the segment text and adjusting frame size, text justification, word wrap and font size
NSArray *views = [view subviews];
int numSubviews = views.count;
for (int i=0; i<numSubviews; i++) {
UIView *thisView = [views objectAtIndex:i];
// Typecast thisView to see if it is a UILabel from one of the segment controls
UILabel *tmpLabel = (UILabel *) thisView;
if ([tmpLabel respondsToSelector:#selector(text)]) {
// Enlarge frame. Segments are set wider and narrower to accomodate the text.
CGRect segmentFrame = [tmpLabel frame];
// The following origin values were necessary to avoid text movement upon making an initial selection but became unnecessary after switching to a bar style segmented control
// segmentFrame.origin.x = 1;
// segmentFrame.origin.y = -1;
segmentFrame.size.height = 40;
// Frame widths are set equal to 2 points less than segment widths set in viewDidLoad
if ([[tmpLabel text] isEqualToString:#"Navel Lint Collector"]) {
segmentFrame.size.width = 64;
}
else if([[tmpLabel text] isEqualToString:#"Deep Thinker"]) {
segmentFrame.size.width = 46;
}
else if([[tmpLabel text] isEqualToString:#"Mental Wizard"]) {
segmentFrame.size.width = 47;
}
else if([[tmpLabel text] isEqualToString:#"Brilliant Professor"]) {
segmentFrame.size.width = 62;
}
else {
// #"Nobel Laureate"
segmentFrame.size.width = 56;
}
[tmpLabel setFrame:segmentFrame];
[tmpLabel setNumberOfLines:0]; // Change from the default of 1 line to 0 meaning use as many lines as needed
[tmpLabel setTextAlignment:UITextAlignmentCenter];
[tmpLabel setFont:[UIFont boldSystemFontOfSize:12]];
[tmpLabel setLineBreakMode:UILineBreakModeWordWrap];
}
if (thisView.subviews.count) {
[self adjustSegmentText:thisView];
}
}
}
The segmented control label text has an ugly appearance in IB but comes out perfectly centered and wrapped across 2 lines on the device and in the simulator using the above code.

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]]; });

MKAnnotationView Doesn't Stay In Place When Zooming

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).