From 39669243b742a399818e5152c873ca71fa971648 Mon Sep 17 00:00:00 2001 From: Jesse Bounds Date: Fri, 22 Apr 2016 07:43:55 -0600 Subject: [PATCH 1/3] [ios] Introduce MGLAnnotationView and support for view annotations Add an UIView subclass that can be used as the base class for all client provided UIViews for annotations. Teach MGLMapView to be able to display annotation views over the map if provided by the client delegate. For now, if the delegate provides a UIView then it will be used. If not, the map view will fall back to the old strategy of using GL annotations with an image provided by the delegate or a default image if not. The map keeps a reuse queue and will store annotation views that are panned offscreen in the queue if the application developer supplied a reuse queue identifer. The views in the queue are reused when more annotation views are required. This view reuse provides a performance gain when many annotations are shown and most of them are offscreen. iosapp now implements the new delegate method to supply a native view. Add a playground to the workspace to facilitate experimentation with new features. A playground is capable of importing frameworks if it exists in the same workspace that builds the imported framework. The initial playground demonstrates annotation views. This also fixes a crash due to nullptr in annotations array If the `annotations` method is called while the user dot's callout view was showing, the userdot annotation is represented as null in the annotation context map. This caused a crash when the null pointer was attempted to be converted into an NSArray via C array. This protects against this bug by filtering out such a null annotation. --- .gitignore | 1 + platform/ios/Mapbox.playground/Contents.swift | 133 +++++++++ .../Mapbox.playground/contents.xcplayground | 4 + .../ios/Mapbox.playground/timeline.xctimeline | 6 + platform/ios/app/MBXAnnotationView.h | 7 + platform/ios/app/MBXAnnotationView.m | 28 ++ platform/ios/app/MBXViewController.m | 20 +- platform/ios/ios.xcodeproj/project.pbxproj | 32 +++ .../xcshareddata/xcschemes/iosapp.xcscheme | 5 +- platform/ios/src/MGLAnnotationView.h | 20 ++ platform/ios/src/MGLAnnotationView.m | 40 +++ platform/ios/src/MGLAnnotationView_Private.h | 13 + platform/ios/src/MGLMapView.h | 5 + platform/ios/src/MGLMapView.mm | 272 ++++++++++++++++-- platform/ios/src/Mapbox.h | 1 + 15 files changed, 554 insertions(+), 33 deletions(-) create mode 100644 platform/ios/Mapbox.playground/Contents.swift create mode 100644 platform/ios/Mapbox.playground/contents.xcplayground create mode 100644 platform/ios/Mapbox.playground/timeline.xctimeline create mode 100644 platform/ios/app/MBXAnnotationView.h create mode 100644 platform/ios/app/MBXAnnotationView.m create mode 100644 platform/ios/src/MGLAnnotationView.h create mode 100644 platform/ios/src/MGLAnnotationView.m create mode 100644 platform/ios/src/MGLAnnotationView_Private.h diff --git a/.gitignore b/.gitignore index 91df4776b79..d887dd3f90a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ xcuserdata /node_modules /platform/ios/benchmark/assets/glyphs/DIN* /platform/ios/benchmark/assets/tiles/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v6 +**/token diff --git a/platform/ios/Mapbox.playground/Contents.swift b/platform/ios/Mapbox.playground/Contents.swift new file mode 100644 index 00000000000..14dba0cc4ee --- /dev/null +++ b/platform/ios/Mapbox.playground/Contents.swift @@ -0,0 +1,133 @@ +import UIKit +import XCPlayground +import Mapbox + +let width: CGFloat = 700 +let height: CGFloat = 800 + +//: A control panel +let panelWidth: CGFloat = 200 +let panel = UIView(frame: CGRect(x: width - panelWidth, y: 0, width: 200, height: 100)) +panel.alpha = 0.8 +panel.backgroundColor = UIColor.whiteColor() +let deleteSwitchLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 30)) +deleteSwitchLabel.adjustsFontSizeToFitWidth = true +deleteSwitchLabel.text = "Delete Markers" +let deleteMarkerSwitchView = UISwitch(frame: CGRect(x: panelWidth - panelWidth / 2.0, y:0, width: 100, height: 50)) +panel.addSubview(deleteSwitchLabel) +panel.addSubview(deleteMarkerSwitchView) +let hideSwitchLabel = UILabel(frame: CGRect(x: 0, y: 30, width: 100, height: 30)) +hideSwitchLabel.adjustsFontSizeToFitWidth = true +hideSwitchLabel.text = "Hide Markers" +let hideMarkerSwitchView = UISwitch(frame: CGRect(x: panelWidth - panelWidth / 2.0, y: 30, width: 100, height: 50)) +panel.addSubview(hideSwitchLabel) +panel.addSubview(hideMarkerSwitchView) + +//: # Mapbox Maps + +/*: + Put your access token into a plain text file called `token`. Then select the “token” placeholder below, go to Editor ‣ Insert File Literal, and select the `token` file. + */ +var accessToken = try String(contentsOfURL: <#token#>) +MGLAccountManager.setAccessToken(accessToken) + +class PlaygroundAnnotationView: MGLAnnotationView { + + override func prepareForReuse() { + hidden = hideMarkerSwitchView.on + } + +} + +//: Define a map delegate + +class MapDelegate: NSObject, MGLMapViewDelegate { + + var annotationViewByAnnotation = [MGLPointAnnotation: PlaygroundAnnotationView]() + + func mapView(mapView: MGLMapView, viewForAnnotation annotation: MGLAnnotation) -> MGLAnnotationView? { + + var annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier("annotation") as? PlaygroundAnnotationView + + if (annotationView == nil) { + let av = PlaygroundAnnotationView(reuseIdentifier: "annotation") + av.frame = CGRect(x: 0, y: 0, width: 30, height: 30) + let centerView = UIView(frame: CGRectInset(av.bounds, 3, 3)) + centerView.backgroundColor = UIColor.whiteColor() + av.addSubview(centerView) + av.backgroundColor = UIColor.purpleColor() + annotationView = av + } else { + annotationView!.subviews.first?.backgroundColor = UIColor.greenColor() + } + + annotationViewByAnnotation[annotation as! MGLPointAnnotation] = annotationView + + return annotationView + } + + func mapView(mapView: MGLMapView, didSelectAnnotation annotation: MGLAnnotation) { + let pointAnnotation = annotation as! MGLPointAnnotation + let annotationView: PlaygroundAnnotationView = annotationViewByAnnotation[pointAnnotation]! + + for view in annotationViewByAnnotation.values { + view.layer.zPosition = -1 + } + + annotationView.layer.zPosition = 1 + + UIView.animateWithDuration(1.25, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.6, options: .CurveEaseOut, animations: { + annotationView.transform = CGAffineTransformMakeScale(1.8, 1.8) + }) { _ in + annotationView.transform = CGAffineTransformMakeScale(1, 1) + + if deleteMarkerSwitchView.on { + mapView.removeAnnotation(pointAnnotation) + return + } + + if hideMarkerSwitchView.on { + annotationView.hidden = true + } + } + } + + func handleTap(press: UILongPressGestureRecognizer) { + let mapView: MGLMapView = press.view as! MGLMapView + + if (press.state == .Recognized) { + let coordiante: CLLocationCoordinate2D = mapView.convertPoint(press.locationInView(mapView), toCoordinateFromView: mapView) + let annotation = MGLPointAnnotation() + annotation.title = "Dropped Marker" + annotation.coordinate = coordiante + mapView.addAnnotation(annotation) + mapView.showAnnotations([annotation], animated: true) + } + } + +} + +//: Create a map and its delegate + +let lat: CLLocationDegrees = 37.174057 +let lng: CLLocationDegrees = -104.490984 +let centerCoordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng) + +let mapView = MGLMapView(frame: CGRect(x: 0, y: 0, width: width, height: height)) +mapView.frame = CGRect(x: 0, y: 0, width: width, height: height) + +XCPlaygroundPage.currentPage.liveView = mapView + +let mapDelegate = MapDelegate() +mapView.delegate = mapDelegate + +let tapGesture = UILongPressGestureRecognizer(target: mapDelegate, action: #selector(mapDelegate.handleTap)) +mapView.addGestureRecognizer(tapGesture) + +//: Zoom in to a location + +mapView.setCenterCoordinate(centerCoordinate, zoomLevel: 12, animated: false) + +//: Add control panel + +mapView.addSubview(panel) diff --git a/platform/ios/Mapbox.playground/contents.xcplayground b/platform/ios/Mapbox.playground/contents.xcplayground new file mode 100644 index 00000000000..35968656f50 --- /dev/null +++ b/platform/ios/Mapbox.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/platform/ios/Mapbox.playground/timeline.xctimeline b/platform/ios/Mapbox.playground/timeline.xctimeline new file mode 100644 index 00000000000..bf468afecac --- /dev/null +++ b/platform/ios/Mapbox.playground/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + + diff --git a/platform/ios/app/MBXAnnotationView.h b/platform/ios/app/MBXAnnotationView.h new file mode 100644 index 00000000000..78dfe176991 --- /dev/null +++ b/platform/ios/app/MBXAnnotationView.h @@ -0,0 +1,7 @@ +#import + +@interface MBXAnnotationView : MGLAnnotationView + +@property (nonatomic) UIColor *centerColor; + +@end diff --git a/platform/ios/app/MBXAnnotationView.m b/platform/ios/app/MBXAnnotationView.m new file mode 100644 index 00000000000..8cbe07a3674 --- /dev/null +++ b/platform/ios/app/MBXAnnotationView.m @@ -0,0 +1,28 @@ +#import "MBXAnnotationView.h" + +@interface MBXAnnotationView () + +@property (nonatomic) UIView *centerView; + +@end + +@implementation MBXAnnotationView + +- (void)layoutSubviews { + [super layoutSubviews]; + if (!self.centerView) { + self.backgroundColor = [UIColor blueColor]; + self.centerView = [[UIView alloc] initWithFrame:CGRectInset(self.bounds, 5.0, 5.0)]; + self.centerView.backgroundColor = self.centerColor; + [self addSubview:self.centerView]; + } +} + +- (void)setCenterColor:(UIColor *)centerColor { + if (![_centerColor isEqual:centerColor]) { + _centerColor = centerColor; + self.centerView.backgroundColor = centerColor; + } +} + +@end diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 88117de1a6d..64d8c63d47f 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -3,6 +3,7 @@ #import "MBXAppDelegate.h" #import "MBXCustomCalloutView.h" #import "MBXOfflinePacksTableViewController.h" +#import "MBXAnnotationView.h" #import @@ -17,6 +18,8 @@ { .latitude = -13.15589555, .longitude = -74.2178961777998 }, }; +static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXViewControllerAnnotationViewReuseIdentifer"; + @interface MBXDroppedPinAnnotation : MGLPointAnnotation @end @@ -461,7 +464,7 @@ - (IBAction)handleLongPress:(UILongPressGestureRecognizer *)longPress toCoordinateFromView:self.mapView]; point.title = @"Dropped Pin"; point.subtitle = [[[MGLCoordinateFormatter alloc] init] stringFromCoordinate:point.coordinate]; - [self.mapView addAnnotation:point]; + // Calling `addAnnotation:` on mapView is not required since `selectAnnotation:animated` has the side effect of adding the annotation if required [self.mapView selectAnnotation:point animated:YES]; } } @@ -608,6 +611,21 @@ - (void)dealloc #pragma mark - MGLMapViewDelegate +- (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id)annotation +{ + MBXAnnotationView *annotationView = (MBXAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:MBXViewControllerAnnotationViewReuseIdentifer]; + if (!annotationView) + { + annotationView = [[MBXAnnotationView alloc] initWithReuseIdentifier:MBXViewControllerAnnotationViewReuseIdentifer]; + annotationView.frame = CGRectMake(0, 0, 40, 40); + annotationView.centerColor = [UIColor whiteColor]; + } else { + // orange indicates that the annotation view was reused + annotationView.centerColor = [UIColor orangeColor]; + } + return annotationView; +} + - (MGLAnnotationImage *)mapView:(MGLMapView * __nonnull)mapView imageForAnnotation:(id __nonnull)annotation { if ([annotation isKindOfClass:[MBXDroppedPinAnnotation class]] diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 8265f1c2d89..6271fc455b4 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 4018B1C71CDC287F00F666AF /* MGLAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.m */; }; + 4018B1C81CDC287F00F666AF /* MGLAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.m */; }; + 4018B1C91CDC288A00F666AF /* MGLAnnotationView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */; }; + 4018B1CA1CDC288E00F666AF /* MGLAnnotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4018B1CB1CDC288E00F666AF /* MGLAnnotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 40FDA76B1CCAAA6800442548 /* MBXAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 40FDA76A1CCAAA6800442548 /* MBXAnnotationView.m */; }; DA17BE301CC4BAC300402C41 /* MGLMapView_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = DA17BE2F1CC4BAC300402C41 /* MGLMapView_Internal.h */; }; DA17BE311CC4BDAA00402C41 /* MGLMapView_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = DA17BE2F1CC4BAC300402C41 /* MGLMapView_Internal.h */; }; DA1DC96A1CB6C6B7006E619F /* MBXCustomCalloutView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA1DC9671CB6C6B7006E619F /* MBXCustomCalloutView.m */; }; @@ -304,6 +310,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAnnotationView_Private.h; sourceTree = ""; }; + 4018B1C41CDC277F00F666AF /* MGLAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLAnnotationView.m; sourceTree = ""; }; + 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAnnotationView.h; sourceTree = ""; }; + 402E9DE01CD2C76200FD4519 /* Mapbox.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Mapbox.playground; sourceTree = ""; }; + 40FDA7691CCAAA6800442548 /* MBXAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBXAnnotationView.h; sourceTree = ""; }; + 40FDA76A1CCAAA6800442548 /* MBXAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBXAnnotationView.m; sourceTree = ""; }; DA17BE2F1CC4BAC300402C41 /* MGLMapView_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapView_Internal.h; sourceTree = ""; }; DA1DC94A1CB6C1C2006E619F /* Mapbox GL.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mapbox GL.app"; sourceTree = BUILT_PRODUCTS_DIR; }; DA1DC9501CB6C1C2006E619F /* MBXAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBXAppDelegate.h; sourceTree = ""; }; @@ -511,9 +523,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 402E9DE21CD3A56500FD4519 /* Playground */ = { + isa = PBXGroup; + children = ( + 402E9DE01CD2C76200FD4519 /* Mapbox.playground */, + ); + name = Playground; + sourceTree = ""; + }; DA1DC9411CB6C1C2006E619F = { isa = PBXGroup; children = ( + 402E9DE21CD3A56500FD4519 /* Playground */, DA1DC94C1CB6C1C2006E619F /* Demo App */, DABCABA91CB80692000A7C39 /* Benchmarking App */, DA8847D31CBAF91600AB86E3 /* SDK */, @@ -543,6 +564,8 @@ children = ( DA1DC9501CB6C1C2006E619F /* MBXAppDelegate.h */, DA1DC9981CB6E054006E619F /* MBXAppDelegate.m */, + 40FDA7691CCAAA6800442548 /* MBXAnnotationView.h */, + 40FDA76A1CCAAA6800442548 /* MBXAnnotationView.m */, DA1DC9661CB6C6B7006E619F /* MBXCustomCalloutView.h */, DA1DC9671CB6C6B7006E619F /* MBXCustomCalloutView.m */, DA1DC9681CB6C6B7006E619F /* MBXOfflinePacksTableViewController.h */, @@ -690,6 +713,9 @@ DA8848331CBAFB2A00AB86E3 /* Kit */ = { isa = PBXGroup; children = ( + 4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */, + 4018B1C41CDC277F00F666AF /* MGLAnnotationView.m */, + 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */, DA8848341CBAFB8500AB86E3 /* MGLAnnotationImage.h */, DA8848401CBAFB9800AB86E3 /* MGLAnnotationImage_Private.h */, DA8848411CBAFB9800AB86E3 /* MGLAnnotationImage.m */, @@ -844,6 +870,7 @@ DA8848861CBB033F00AB86E3 /* Fabric+FABKits.h in Headers */, DA8848201CBAFA6200AB86E3 /* MGLOfflinePack_Private.h in Headers */, DA8847FA1CBAFA5100AB86E3 /* MGLPolyline.h in Headers */, + 4018B1C91CDC288A00F666AF /* MGLAnnotationView_Private.h in Headers */, DA88482C1CBAFA6200AB86E3 /* NSBundle+MGLAdditions.h in Headers */, DA88488E1CBB047F00AB86E3 /* reachability.h in Headers */, DA8848231CBAFA6200AB86E3 /* MGLOfflineStorage_Private.h in Headers */, @@ -855,6 +882,7 @@ DA8847FC1CBAFA5100AB86E3 /* MGLStyle.h in Headers */, DA8847F01CBAFA5100AB86E3 /* MGLAnnotation.h in Headers */, DA88483E1CBAFB8500AB86E3 /* MGLMapView+MGLCustomStyleLayerAdditions.h in Headers */, + 4018B1CA1CDC288E00F666AF /* MGLAnnotationView.h in Headers */, DA8847EF1CBAFA5100AB86E3 /* MGLAccountManager.h in Headers */, DA8848511CBAFB9800AB86E3 /* MGLAPIClient.h in Headers */, DA35A2C91CCAAAD200E826B2 /* NSValue+MGLAdditions.h in Headers */, @@ -919,6 +947,7 @@ DA35A2B21CCA141D00E826B2 /* MGLCompassDirectionFormatter.h in Headers */, DABFB8731CBE9A9900D62B32 /* Mapbox.h in Headers */, DABFB86B1CBE99E500D62B32 /* MGLTilePyramidOfflineRegion.h in Headers */, + 4018B1CB1CDC288E00F666AF /* MGLAnnotationView.h in Headers */, DABFB85F1CBE99E500D62B32 /* MGLGeometry.h in Headers */, DABFB85D1CBE99E500D62B32 /* MGLAccountManager.h in Headers */, DA35A2BC1CCA9A6900E826B2 /* MGLClockDirectionFormatter.h in Headers */, @@ -1217,6 +1246,7 @@ DA1DC96B1CB6C6B7006E619F /* MBXOfflinePacksTableViewController.m in Sources */, DA1DC96A1CB6C6B7006E619F /* MBXCustomCalloutView.m in Sources */, DA1DC99B1CB6E064006E619F /* MBXViewController.m in Sources */, + 40FDA76B1CCAAA6800442548 /* MBXAnnotationView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1259,6 +1289,7 @@ DA88481C1CBAFA6200AB86E3 /* MGLGeometry.mm in Sources */, DA88481F1CBAFA6200AB86E3 /* MGLMultiPoint.mm in Sources */, DA88482B1CBAFA6200AB86E3 /* MGLTypes.m in Sources */, + 4018B1C71CDC287F00F666AF /* MGLAnnotationView.m in Sources */, DA88481D1CBAFA6200AB86E3 /* MGLMapCamera.mm in Sources */, DA8848261CBAFA6200AB86E3 /* MGLPolygon.mm in Sources */, DA8848521CBAFB9800AB86E3 /* MGLAPIClient.m in Sources */, @@ -1296,6 +1327,7 @@ DAA4E4301CBB730400178DFB /* MGLLocationManager.m in Sources */, DAA4E4321CBB730400178DFB /* MGLMapView.mm in Sources */, DAA4E41E1CBB730400178DFB /* MGLMapCamera.mm in Sources */, + 4018B1C81CDC287F00F666AF /* MGLAnnotationView.m in Sources */, DAA4E4341CBB730400178DFB /* MGLUserLocationAnnotationView.m in Sources */, DAA4E42C1CBB730400178DFB /* reachability.m in Sources */, DAA4E4311CBB730400178DFB /* MGLMapboxEvents.m in Sources */, diff --git a/platform/ios/ios.xcodeproj/xcshareddata/xcschemes/iosapp.xcscheme b/platform/ios/ios.xcodeproj/xcshareddata/xcschemes/iosapp.xcscheme index 064add0fea3..de74d64314c 100644 --- a/platform/ios/ios.xcodeproj/xcshareddata/xcschemes/iosapp.xcscheme +++ b/platform/ios/ios.xcodeproj/xcshareddata/xcschemes/iosapp.xcscheme @@ -70,7 +70,8 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> - + - + diff --git a/platform/ios/src/MGLAnnotationView.h b/platform/ios/src/MGLAnnotationView.h new file mode 100644 index 00000000000..475d9aa758b --- /dev/null +++ b/platform/ios/src/MGLAnnotationView.h @@ -0,0 +1,20 @@ +#import + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MGLAnnotationView : UIView + +- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier; + +/** + The string that identifies that this annotation view is reusable. (read-only) + */ +@property (nonatomic, readonly, nullable) NSString *reuseIdentifier; + +- (void)prepareForReuse; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLAnnotationView.m b/platform/ios/src/MGLAnnotationView.m new file mode 100644 index 00000000000..9a82a8b3324 --- /dev/null +++ b/platform/ios/src/MGLAnnotationView.m @@ -0,0 +1,40 @@ +#import "MGLAnnotationView.h" +#import "MGLAnnotationView_Private.h" + +@interface MGLAnnotationView () + +@property (nonatomic) id annotation; +@property (nonatomic, readwrite, nullable) NSString *reuseIdentifier; + +@end + +@implementation MGLAnnotationView + +- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super init]; + + if (self) + { + _reuseIdentifier = [reuseIdentifier copy]; + } + + return self; +} + +- (void)prepareForReuse +{ + // Intentionally left blank. The default implementation of this method does nothing. +} + +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event +{ + // Allow mbgl to drive animation of this view’s bounds. + if ([event isEqualToString:@"bounds"]) + { + return [NSNull null]; + } + return [super actionForLayer:layer forKey:event]; +} + +@end \ No newline at end of file diff --git a/platform/ios/src/MGLAnnotationView_Private.h b/platform/ios/src/MGLAnnotationView_Private.h new file mode 100644 index 00000000000..c9a887b6cc7 --- /dev/null +++ b/platform/ios/src/MGLAnnotationView_Private.h @@ -0,0 +1,13 @@ +#import "MGLAnnotationView.h" +#import "MGLAnnotation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MGLAnnotationView (Private) + +@property (nonatomic) id annotation; +@property (nonatomic, readwrite, nullable) NSString *reuseIdentifier; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index 0339a5457b6..94355649429 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -8,6 +8,7 @@ NS_ASSUME_NONNULL_BEGIN +@class MGLAnnotationView; @class MGLAnnotationImage; @class MGLUserLocation; @class MGLPolyline; @@ -931,6 +932,8 @@ IB_DESIGNABLE */ - (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier; +- (nullable MGLAnnotationView *)dequeueReusableAnnotationViewWithIdentifier:(NSString *)identifier; + #pragma mark Managing Annotation Selections /** @@ -1151,6 +1154,8 @@ IB_DESIGNABLE #pragma mark Managing the Display of Annotations +- (nullable MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id )annotation; + /** Returns an image object to use for the marker for the specified point annotation object. diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index a0c24eebe5e..742e247fc5e 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -37,6 +37,7 @@ #import "MGLUserLocationAnnotationView.h" #import "MGLUserLocation_Private.h" #import "MGLAnnotationImage_Private.h" +#import "MGLAnnotationView_Private.h" #import "MGLMapboxEvents.h" #import "MGLCompactCalloutView.h" @@ -167,6 +168,8 @@ - (void)accessibilityDecrement /// The annotation’s image’s reuse identifier. NSString *imageReuseIdentifier; MGLAnnotationAccessibilityElement *accessibilityElement; + MGLAnnotationView *annotationView; + NSString *viewReuseIdentifier; }; /** An accessibility element representing the MGLMapView at large. */ @@ -240,10 +243,12 @@ @implementation MGLMapView BOOL _opaque; NS_MUTABLE_ARRAY_OF(NSURL *) *_bundledStyleURLs; - + MGLAnnotationContextMap _annotationContextsByAnnotationTag; /// Tag of the selected annotation. If the user location annotation is selected, this ivar is set to `MGLAnnotationTagNotFound`. MGLAnnotationTag _selectedAnnotationTag; + NS_MUTABLE_DICTIONARY_OF(NSString *, NS_MUTABLE_ARRAY_OF(MGLAnnotationView *) *) *_annotationViewReuseQueueByIdentifier; + BOOL _userLocationAnnotationIsSelected; /// Size of the rectangle formed by unioning the maximum slop area around every annotation image. CGSize _unionedAnnotationImageSize; @@ -272,6 +277,8 @@ @implementation MGLMapView BOOL _delegateHasLineWidthsForShapeAnnotations; MGLCompassDirectionFormatter *_accessibilityCompassFormatter; + + CGSize _largestAnnotationViewSize; } #pragma mark - Setup & Teardown - @@ -404,6 +411,7 @@ - (void)commonInit // Set up annotation management and selection state. _annotationImagesByIdentifier = [NSMutableDictionary dictionary]; _annotationContextsByAnnotationTag = {}; + _annotationViewReuseQueueByIdentifier = [NSMutableDictionary dictionary]; _selectedAnnotationTag = MGLAnnotationTagNotFound; _annotationsNearbyLastTap = {}; @@ -1404,6 +1412,13 @@ - (void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap } return; } + + MGLAnnotationView *hitAnnotationView = [self annotationViewAtPoint:tapPoint]; + if (hitAnnotationView) + { + [self selectAnnotation:hitAnnotationView.annotation animated:YES]; + return; + } MGLAnnotationTag hitAnnotationTag = [self annotationTagAtPoint:tapPoint persistingResults:YES]; if (hitAnnotationTag != MGLAnnotationTagNotFound) @@ -2713,6 +2728,11 @@ - (void)removeStyleClass:(NSString *)styleClass { return pair.second.annotation; }); + + annotations.erase(std::remove_if(annotations.begin(), annotations.end(), + [](const id annotation) { return annotation == nullptr; }), + annotations.end()); + return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()]; } @@ -2763,8 +2783,11 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations std::vector points; std::vector shapes; - NSMutableArray *annotationImages = [NSMutableArray arrayWithCapacity:annotations.count]; + + NSMutableDictionary *annotationImagesForAnnotation = [NSMutableDictionary dictionary]; + NSMutableDictionary *annotationViewsForAnnotation = [NSMutableDictionary dictionary]; + BOOL delegateImplementsViewForAnnotation = [self.delegate respondsToSelector:@selector(mapView:viewForAnnotation:)]; BOOL delegateImplementsImageForPoint = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)]; for (id annotation in annotations) @@ -2777,32 +2800,51 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations } else { - MGLAnnotationImage *annotationImage; - if (delegateImplementsImageForPoint) - { - annotationImage = [self.delegate mapView:self imageForAnnotation:annotation]; - } - if ( ! annotationImage) - { - annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]; - } - if ( ! annotationImage) - { - annotationImage = self.defaultAnnotationImage; - } + MGLAnnotationView *annotationView; + NSString *symbolName; + NSValue *annotationValue = [NSValue valueWithNonretainedObject:annotation]; - NSString *symbolName = annotationImage.styleIconIdentifier; - if ( ! symbolName) + if (delegateImplementsViewForAnnotation) { - symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; - annotationImage.styleIconIdentifier = symbolName; + annotationView = [self annotationViewForAnnotation:annotation]; + if (annotationView) + { + annotationViewsForAnnotation[annotationValue] = annotationView; + annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self]; + [self.glView addSubview:annotationView]; + } } - if ( ! self.annotationImagesByIdentifier[annotationImage.reuseIdentifier]) - { - [self installAnnotationImage:annotationImage]; + if ( ! annotationView) { + MGLAnnotationImage *annotationImage; + + if (delegateImplementsImageForPoint) + { + annotationImage = [self.delegate mapView:self imageForAnnotation:annotation]; + } + if ( ! annotationImage) + { + annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]; + } + if ( ! annotationImage) + { + annotationImage = self.defaultAnnotationImage; + } + + symbolName = annotationImage.styleIconIdentifier; + + if ( ! symbolName) + { + symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; + annotationImage.styleIconIdentifier = symbolName; + } + if ( ! self.annotationImagesByIdentifier[annotationImage.reuseIdentifier]) + { + [self installAnnotationImage:annotationImage]; + } + + annotationImagesForAnnotation[annotationValue] = annotationImage; } - [annotationImages addObject:annotationImage]; points.emplace_back(MGLLatLngFromLocationCoordinate2D(annotation.coordinate), symbolName.UTF8String ?: ""); } @@ -2810,20 +2852,30 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations if (points.size()) { + // refactor this to build contexts above and just associate with tags here + std::vector annotationTags = _mbglMap->addPointAnnotations(points); for (size_t i = 0; i < annotationTags.size(); ++i) { - MGLAnnotationTag annotationTag = annotationTags[i]; - MGLAnnotationImage *annotationImage = annotationImages[i]; - annotationImage.styleIconIdentifier = @(points[i].icon.c_str()); - id annotation = annotations[i]; + id annotation = annotations[i]; + NSValue *annotationValue = [NSValue valueWithNonretainedObject:annotation]; MGLAnnotationContext context; context.annotation = annotation; - context.imageReuseIdentifier = annotationImage.reuseIdentifier; - _annotationContextsByAnnotationTag[annotationTag] = context; + MGLAnnotationImage *annotationImage = annotationImagesForAnnotation[annotationValue]; + + if (annotationImage) { + context.imageReuseIdentifier = annotationImage.reuseIdentifier; + } + MGLAnnotationView *annotationView = annotationViewsForAnnotation[annotationValue]; + if (annotationView) { + context.annotationView = annotationView; + context.viewReuseIdentifier = annotationView.reuseIdentifier; + } + MGLAnnotationTag annotationTag = annotationTags[i]; + _annotationContextsByAnnotationTag[annotationTag] = context; if ([annotation isKindOfClass:[NSObject class]]) { NSAssert(![annotation isKindOfClass:[MGLMultiPoint class]], @"Point annotation should not be MGLMultiPoint."); [(NSObject *)annotation addObserver:self forKeyPath:@"coordinate" options:0 context:(void *)(NSUInteger)annotationTag]; @@ -2864,6 +2916,20 @@ - (MGLAnnotationImage *)defaultAnnotationImage return annotationImage; } +- (MGLAnnotationView *)annotationViewForAnnotation:(id)annotation +{ + MGLAnnotationView *annotationView = [self.delegate mapView:self viewForAnnotation:annotation]; + + if (annotationView) + { + annotationView.annotation = annotation; + CGRect bounds = UIEdgeInsetsInsetRect({ CGPointZero, annotationView.frame.size }, annotationView.alignmentRectInsets); + _largestAnnotationViewSize = CGSizeMake(bounds.size.width / 2.0, bounds.size.height / 2.0); + } + + return annotationView; +} + - (double)alphaForShapeAnnotation:(MGLShape *)annotation { if (_delegateHasAlphasForShapeAnnotations) @@ -2962,6 +3028,11 @@ - (void)removeAnnotations:(NS_ARRAY_OF(id ) *)annotations { continue; } + + MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); + MGLAnnotationView *annotationView = annotationContext.annotationView; + [annotationView removeFromSuperview]; + annotationTagsToRemove.push_back(annotationTag); if (annotationTag == _selectedAnnotationTag) @@ -3025,6 +3096,35 @@ - (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(N return self.annotationImagesByIdentifier[identifier]; } +- (nullable MGLAnnotationView *)dequeueReusableAnnotationViewWithIdentifier:(NSString *)identifier +{ + NSMutableArray *annotationViewReuseQueue = [self annotationViewReuseQueueForIdentifier:identifier]; + MGLAnnotationView *reusableView = annotationViewReuseQueue.firstObject; + [reusableView prepareForReuse]; + [annotationViewReuseQueue removeObject:reusableView]; + + return reusableView; +} + +- (MGLAnnotationView *)annotationViewAtPoint:(CGPoint)point +{ + std::vector annotationTags = [self annotationTagsInRect:self.bounds]; + + for(auto const& annotationTag: annotationTags) + { + auto &annotationContext = _annotationContextsByAnnotationTag[annotationTag]; + MGLAnnotationView *annotationView = annotationContext.annotationView; + CGPoint convertedPoint = [self convertPoint:point toView:annotationView]; + + if ([annotationView pointInside:convertedPoint withEvent:nil]) + { + return annotationView; + } + } + + return nil; +} + /** Returns the tag of the annotation at the given point in the view. @@ -3067,6 +3167,8 @@ - (MGLAnnotationTag)annotationTagAtPoint:(CGPoint)point persistingResults:(BOOL) id annotation = [self annotationWithTag:annotationTag]; NSAssert(annotation, @"Unknown annotation found nearby tap"); + MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag[annotationTag]; + MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; if ( ! annotationImage.enabled) { @@ -3076,8 +3178,10 @@ - (MGLAnnotationTag)annotationTagAtPoint:(CGPoint)point persistingResults:(BOOL) // Filter out the annotation if the fattened finger didn’t land // within the image’s alignment rect. CGRect annotationRect = [self frameOfImage:annotationImage.image ?: fallbackImage centeredAtCoordinate:annotation.coordinate]; + return !!!CGRectIntersectsRect(annotationRect, hitRect); }); + nearbyAnnotations.resize(std::distance(nearbyAnnotations.begin(), end)); } @@ -3224,10 +3328,26 @@ - (void)selectAnnotation:(id )annotation animated:(BOOL)animated if (annotationTag == MGLAnnotationTagNotFound && annotation != self.userLocation) { [self addAnnotation:annotation]; + annotationTag = [self annotationTagForAnnotation:annotation]; + if (annotationTag == MGLAnnotationTagNotFound) return; } - // The annotation can’t be selected if no part of it is hittable. + // By default attempt to use the GL annotation image frame as the positioning rect. CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; + + if (annotation != self.userLocation) + { + MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); + + if (annotationContext.annotationView) + { + // Annotations represented by views use the view frame as the positioning rect. + positioningRect = annotationContext.annotationView.frame; + } + } + + // The client can request that any annotation be selected (even ones that are offscreen). + // The annotation can’t be selected if no part of it is hittable. if ( ! CGRectIntersectsRect(positioningRect, self.bounds) && annotation != self.userLocation) { return; @@ -3320,6 +3440,8 @@ - (MGLCompactCalloutView *)calloutViewForAnnotation:(id )annotati /// and is appropriate for positioning a popover. - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annotationTag { + MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag[annotationTag]; + id annotation = [self annotationWithTag:annotationTag]; if ( ! annotation) { @@ -3337,6 +3459,7 @@ - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annota CGRect positioningRect = [self frameOfImage:image centeredAtCoordinate:annotation.coordinate]; positioningRect.origin.x -= 0.5; + return CGRectInset(positioningRect, -MGLAnnotationImagePaddingForCallout, -MGLAnnotationImagePaddingForCallout); } @@ -4221,6 +4344,7 @@ - (void)notifyMapChange:(mbgl::MapChange)change { [self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:(change == mbgl::MapChangeDidFinishRenderingFrameFullyRendered)]; } + [self updateAnnotationViews]; break; } } @@ -4231,6 +4355,85 @@ - (void)updateUserLocationAnnotationView [self updateUserLocationAnnotationViewAnimatedWithDuration:0]; } +- (void)updateAnnotationViews +{ + BOOL delegateImplementsViewForAnnotation = [self.delegate respondsToSelector:@selector(mapView:viewForAnnotation:)]; + + if (!delegateImplementsViewForAnnotation) + { + return; + } + + // Update all visible annotation views + std::set visibleTags; + std::vector annotationTags = [self annotationTagsInRect:CGRectInset(self.bounds, -_largestAnnotationViewSize.width - MGLAnnotationUpdateViewportOutset.width, -_largestAnnotationViewSize.height - MGLAnnotationUpdateViewportOutset.width)]; + + for(auto const& annotationTag: annotationTags) + { + auto &annotationContext = _annotationContextsByAnnotationTag[annotationTag]; + id annotation = annotationContext.annotation; + + + // If there is no annotation view at this point, it means the context's view was reused by some + // other context so we need to reuse or make a new view. + if (!annotationContext.annotationView) + { + MGLAnnotationView *annotationView = [self annotationViewForAnnotation:annotation]; + + if (annotationView) + { + // If the annotation view has no superview it means it was never used before so add it + if (!annotationView.superview) + { + [self.glView addSubview:annotationView]; + } + + annotationContext.annotationView = annotationView; + } + } + + annotationContext.annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self];; + visibleTags.insert(annotationTag); + } + + // Hide and add offscreen annotation views to reuse queue + for (auto &pair : _annotationContextsByAnnotationTag) + { + MGLAnnotationTag annotationTag = pair.first; + MGLAnnotationContext &annotationContext = pair.second; + MGLAnnotationView *annotationView = annotationContext.annotationView; + const bool tagIsNotVisible = visibleTags.find(annotationTag) == visibleTags.end(); + + // The call to `annotationTagsInRect:` (above) does not return the correct result when the + // map is tilted and the user is scrolling quickly. So, some annotation views get stuck in + // a limbo state where they are onscreen and put on the reuse queue. Hiding the views hides + // the bug until we fix the result of `annotationTagsInRect:`. + annotationView.hidden = tagIsNotVisible; + + if (annotationView && annotationView.reuseIdentifier && tagIsNotVisible) + { + [self enqueueAnnotationViewForAnnotationContext:annotationContext]; + } + } +} + +- (void)enqueueAnnotationViewForAnnotationContext:(MGLAnnotationContext &)annotationContext +{ + MGLAnnotationView *annotationView = annotationContext.annotationView; + + if (!annotationView) return; + + if (annotationContext.viewReuseIdentifier) + { + NSMutableArray *annotationViewReuseQueue = [self annotationViewReuseQueueForIdentifier:annotationContext.viewReuseIdentifier]; + if (![annotationViewReuseQueue containsObject:annotationView]) + { + [annotationViewReuseQueue addObject:annotationView]; + annotationContext.annotationView = nil; + } + } +} + - (void)updateUserLocationAnnotationViewAnimatedWithDuration:(NSTimeInterval)duration { MGLUserLocationAnnotationView *annotationView = self.userLocationAnnotationView; @@ -4492,6 +4695,15 @@ - (void)prepareForInterfaceBuilder views:views]]; } +- (NS_MUTABLE_ARRAY_OF(MGLAnnotationView *) *)annotationViewReuseQueueForIdentifier:(NSString *)identifier { + if (!_annotationViewReuseQueueByIdentifier[identifier]) + { + _annotationViewReuseQueueByIdentifier[identifier] = [NSMutableArray array]; + } + + return _annotationViewReuseQueueByIdentifier[identifier]; +} + class MBGLView : public mbgl::View { public: diff --git a/platform/ios/src/Mapbox.h b/platform/ios/src/Mapbox.h index df08c1b4a22..3b7361a7562 100644 --- a/platform/ios/src/Mapbox.h +++ b/platform/ios/src/Mapbox.h @@ -6,6 +6,7 @@ FOUNDATION_EXPORT double MapboxVersionNumber; /// Project version string for Mapbox. FOUNDATION_EXPORT const unsigned char MapboxVersionString[]; +#import "MGLAnnotationView.h" #import "MGLAccountManager.h" #import "MGLAnnotation.h" #import "MGLAnnotationImage.h" From fb759e8475906aa6ff6c8aa035446d801216beaf Mon Sep 17 00:00:00 2001 From: Jesse Bounds Date: Thu, 12 May 2016 15:55:22 -0700 Subject: [PATCH 2/3] [ios] Refactor of annotation view updates This change eliminates the call to the problematic `annotationTagsInRect:` by using the annotation views themselves and the map view to check if a view is visible or not. --- platform/ios/src/MGLMapView.mm | 45 ++++++++++------------------------ 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 742e247fc5e..7afe72bf63f 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -4364,22 +4364,16 @@ - (void)updateAnnotationViews return; } - // Update all visible annotation views - std::set visibleTags; - std::vector annotationTags = [self annotationTagsInRect:CGRectInset(self.bounds, -_largestAnnotationViewSize.width - MGLAnnotationUpdateViewportOutset.width, -_largestAnnotationViewSize.height - MGLAnnotationUpdateViewportOutset.width)]; - - for(auto const& annotationTag: annotationTags) + for (auto &pair : _annotationContextsByAnnotationTag) { - auto &annotationContext = _annotationContextsByAnnotationTag[annotationTag]; - id annotation = annotationContext.annotation; + CGRect viewPort = CGRectInset(self.bounds, -_largestAnnotationViewSize.width - MGLAnnotationUpdateViewportOutset.width, -_largestAnnotationViewSize.height - MGLAnnotationUpdateViewportOutset.width); + MGLAnnotationContext &annotationContext = pair.second; + MGLAnnotationView *annotationView = annotationContext.annotationView; - // If there is no annotation view at this point, it means the context's view was reused by some - // other context so we need to reuse or make a new view. - if (!annotationContext.annotationView) + if (!annotationView) { - MGLAnnotationView *annotationView = [self annotationViewForAnnotation:annotation]; - + MGLAnnotationView *annotationView = [self annotationViewForAnnotation:annotationContext.annotation]; if (annotationView) { // If the annotation view has no superview it means it was never used before so add it @@ -4387,33 +4381,20 @@ - (void)updateAnnotationViews { [self.glView addSubview:annotationView]; } - + annotationView.center = [self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self]; annotationContext.annotationView = annotationView; } } - annotationContext.annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self];; - visibleTags.insert(annotationTag); - } - - // Hide and add offscreen annotation views to reuse queue - for (auto &pair : _annotationContextsByAnnotationTag) - { - MGLAnnotationTag annotationTag = pair.first; - MGLAnnotationContext &annotationContext = pair.second; - MGLAnnotationView *annotationView = annotationContext.annotationView; - const bool tagIsNotVisible = visibleTags.find(annotationTag) == visibleTags.end(); - - // The call to `annotationTagsInRect:` (above) does not return the correct result when the - // map is tilted and the user is scrolling quickly. So, some annotation views get stuck in - // a limbo state where they are onscreen and put on the reuse queue. Hiding the views hides - // the bug until we fix the result of `annotationTagsInRect:`. - annotationView.hidden = tagIsNotVisible; - - if (annotationView && annotationView.reuseIdentifier && tagIsNotVisible) + bool annotationViewIsVisible = CGRectContainsRect(viewPort, annotationView.frame); + if (!annotationViewIsVisible) { [self enqueueAnnotationViewForAnnotationContext:annotationContext]; } + else + { + annotationView.center = [self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self];; + } } } From 1ee14915f8484f22abb59b7ecdb48e197e6dbf38 Mon Sep 17 00:00:00 2001 From: Jesse Bounds Date: Fri, 13 May 2016 12:58:28 -0700 Subject: [PATCH 3/3] [ios] Add method documentation for view based annotations. --- platform/ios/src/MGLAnnotationView.h | 20 ++++++++++++++++++++ platform/ios/src/MGLMapView.h | 21 +++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/platform/ios/src/MGLAnnotationView.h b/platform/ios/src/MGLAnnotationView.h index 475d9aa758b..bc2494e9fc5 100644 --- a/platform/ios/src/MGLAnnotationView.h +++ b/platform/ios/src/MGLAnnotationView.h @@ -4,15 +4,35 @@ NS_ASSUME_NONNULL_BEGIN +/** The MGLAnnotationView class is responsible for representing point-based annotation markers as a view. Annotation views represent an annotation object, which is an object that corresponds to the MGLAnnotation protocol. When an annotation’s coordinate point is visible on the map view, the map view delegate is asked to provide a corresponding annotation view. If an annotation view is created with a reuse identifier, the map view may recycle the view when it goes offscreen. */ @interface MGLAnnotationView : UIView +/** + Initializes and returns a new annotation view object. + + @param reuseIdentifier The string that identifies that this annotation view is reusable. + @return The initialized annotation view object or `nil` if there was a problem initializing the object. + */ - (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier; /** The string that identifies that this annotation view is reusable. (read-only) + + You specify the reuse identifier when you create the view. You use the identifier later to retrieve an annotation view that was + created previously but which is currently unused because its annotation is not on screen. + + If you define distinctly different types of annotations (with distinctly different annotation views to go with them), you can + differentiate between the annotation types by specifying different reuse identifiers for each one. */ @property (nonatomic, readonly, nullable) NSString *reuseIdentifier; + +/** + Called when the view is removed from the reuse queue. + + The default implementation of this method does nothing. You can override it in your custom annotation views and use it to put the view + in a known state before it is returned to your map view delegate. + */ - (void)prepareForReuse; @end diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index 94355649429..b6fd1c4ffdb 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -932,6 +932,20 @@ IB_DESIGNABLE */ - (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier; +/** + Returns a reusable annotation view object associated with its identifier. + + For performance reasons, you should generally reuse `MGLAnnotationView` + objects for identical-looking annotations in your map views. Dequeueing + saves time and memory during performance-critical operations such as + scrolling. + + @param identifier A string identifying the annotation view to be reused. + This string is the same one you specify when initially returning the + annotation view object using the `-mapView:viewForAnnotation:` method. + @return An annotation view object with the given identifier, or `nil` if no + such object exists in the reuse queue. + */ - (nullable MGLAnnotationView *)dequeueReusableAnnotationViewWithIdentifier:(NSString *)identifier; #pragma mark Managing Annotation Selections @@ -1154,6 +1168,13 @@ IB_DESIGNABLE #pragma mark Managing the Display of Annotations +/** + Returns a view object to use for the marker for the specified point annotation object. + + @param mapView The map view that requested the annotation view. + @param annotation The object representing the annotation that is about to be displayed. + @return The view object to display for the specified annotation or `nil` if you want to display the default marker image. + */ - (nullable MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id )annotation; /**