diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | platform/ios/Mapbox.playground/Contents.swift | 133 | ||||
-rw-r--r-- | platform/ios/Mapbox.playground/contents.xcplayground | 4 | ||||
-rw-r--r-- | platform/ios/Mapbox.playground/timeline.xctimeline | 6 | ||||
-rw-r--r-- | platform/ios/app/MBXAnnotationView.h | 7 | ||||
-rw-r--r-- | platform/ios/app/MBXAnnotationView.m | 28 | ||||
-rw-r--r-- | platform/ios/app/MBXViewController.m | 20 | ||||
-rw-r--r-- | platform/ios/ios.xcodeproj/project.pbxproj | 32 | ||||
-rw-r--r-- | platform/ios/ios.xcodeproj/xcshareddata/xcschemes/iosapp.xcscheme | 5 | ||||
-rw-r--r-- | platform/ios/src/MGLAnnotationView.h | 20 | ||||
-rw-r--r-- | platform/ios/src/MGLAnnotationView.m | 40 | ||||
-rw-r--r-- | platform/ios/src/MGLAnnotationView_Private.h | 13 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.h | 5 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 272 | ||||
-rw-r--r-- | platform/ios/src/Mapbox.h | 1 |
15 files changed, 554 insertions, 33 deletions
diff --git a/.gitignore b/.gitignore index 91df4776b7..d887dd3f90 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 0000000000..14dba0cc4e --- /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 0000000000..35968656f5 --- /dev/null +++ b/platform/ios/Mapbox.playground/contents.xcplayground @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<playground version='5.0' target-platform='ios' display-mode='raw'> + <timeline fileName='timeline.xctimeline'/> +</playground>
\ 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 0000000000..bf468afeca --- /dev/null +++ b/platform/ios/Mapbox.playground/timeline.xctimeline @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Timeline + version = "3.0"> + <TimelineItems> + </TimelineItems> +</Timeline> diff --git a/platform/ios/app/MBXAnnotationView.h b/platform/ios/app/MBXAnnotationView.h new file mode 100644 index 0000000000..78dfe17699 --- /dev/null +++ b/platform/ios/app/MBXAnnotationView.h @@ -0,0 +1,7 @@ +#import <Mapbox/Mapbox.h> + +@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 0000000000..8cbe07a367 --- /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 88117de1a6..64d8c63d47 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 <Mapbox/Mapbox.h> @@ -17,6 +18,8 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { { .latitude = -13.15589555, .longitude = -74.2178961777998 }, }; +static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXViewControllerAnnotationViewReuseIdentifer"; + @interface MBXDroppedPinAnnotation : MGLPointAnnotation @end @@ -461,7 +464,7 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { 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 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { #pragma mark - MGLMapViewDelegate +- (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id<MGLAnnotation>)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 <MGLAnnotation> __nonnull)annotation { if ([annotation isKindOfClass:[MBXDroppedPinAnnotation class]] diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 8265f1c2d8..6271fc455b 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 = "<group>"; }; + 4018B1C41CDC277F00F666AF /* MGLAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLAnnotationView.m; sourceTree = "<group>"; }; + 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAnnotationView.h; sourceTree = "<group>"; }; + 402E9DE01CD2C76200FD4519 /* Mapbox.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Mapbox.playground; sourceTree = "<group>"; }; + 40FDA7691CCAAA6800442548 /* MBXAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBXAnnotationView.h; sourceTree = "<group>"; }; + 40FDA76A1CCAAA6800442548 /* MBXAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBXAnnotationView.m; sourceTree = "<group>"; }; DA17BE2F1CC4BAC300402C41 /* MGLMapView_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapView_Internal.h; sourceTree = "<group>"; }; 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 = "<group>"; }; @@ -511,9 +523,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 402E9DE21CD3A56500FD4519 /* Playground */ = { + isa = PBXGroup; + children = ( + 402E9DE01CD2C76200FD4519 /* Mapbox.playground */, + ); + name = Playground; + sourceTree = "<group>"; + }; 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 064add0fea..de74d64314 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"> - <MacroExpansion> + <BuildableProductRunnable + runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "DA1DC9491CB6C1C2006E619F" @@ -78,7 +79,7 @@ BlueprintName = "iosapp" ReferencedContainer = "container:ios.xcodeproj"> </BuildableReference> - </MacroExpansion> + </BuildableProductRunnable> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> diff --git a/platform/ios/src/MGLAnnotationView.h b/platform/ios/src/MGLAnnotationView.h new file mode 100644 index 0000000000..475d9aa758 --- /dev/null +++ b/platform/ios/src/MGLAnnotationView.h @@ -0,0 +1,20 @@ +#import <UIKit/UIKit.h> + +#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 0000000000..9a82a8b332 --- /dev/null +++ b/platform/ios/src/MGLAnnotationView.m @@ -0,0 +1,40 @@ +#import "MGLAnnotationView.h" +#import "MGLAnnotationView_Private.h" + +@interface MGLAnnotationView () + +@property (nonatomic) id<MGLAnnotation> 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<CAAction>)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 0000000000..c9a887b6cc --- /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<MGLAnnotation> 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 0339a5457b..9435564942 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 <MGLAnnotation>)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 a0c24eebe5..742e247fc5 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 @@ public: /// 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 @@ public: 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 @@ public: BOOL _delegateHasLineWidthsForShapeAnnotations; MGLCompassDirectionFormatter *_accessibilityCompassFormatter; + + CGSize _largestAnnotationViewSize; } #pragma mark - Setup & Teardown - @@ -404,6 +411,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) // Set up annotation management and selection state. _annotationImagesByIdentifier = [NSMutableDictionary dictionary]; _annotationContextsByAnnotationTag = {}; + _annotationViewReuseQueueByIdentifier = [NSMutableDictionary dictionary]; _selectedAnnotationTag = MGLAnnotationTagNotFound; _annotationsNearbyLastTap = {}; @@ -1404,6 +1412,13 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) } 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 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) { return pair.second.annotation; }); + + annotations.erase(std::remove_if(annotations.begin(), annotations.end(), + [](const id <MGLAnnotation> annotation) { return annotation == nullptr; }), + annotations.end()); + return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()]; } @@ -2763,8 +2783,11 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) std::vector<mbgl::PointAnnotation> points; std::vector<mbgl::ShapeAnnotation> 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 <MGLAnnotation> annotation in annotations) @@ -2777,32 +2800,51 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) } 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 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) if (points.size()) { + // refactor this to build contexts above and just associate with tags here + std::vector<MGLAnnotationTag> 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 <MGLAnnotation> annotation = annotations[i]; + id<MGLAnnotation> 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 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) return annotationImage; } +- (MGLAnnotationView *)annotationViewForAnnotation:(id<MGLAnnotation>)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 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) { continue; } + + MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); + MGLAnnotationView *annotationView = annotationContext.annotationView; + [annotationView removeFromSuperview]; + annotationTagsToRemove.push_back(annotationTag); if (annotationTag == _selectedAnnotationTag) @@ -3025,6 +3096,35 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) 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<MGLAnnotationTag> 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 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) id <MGLAnnotation> 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 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) // 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 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) 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 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) /// and is appropriate for positioning a popover. - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annotationTag { + MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag[annotationTag]; + id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag]; if ( ! annotation) { @@ -3337,6 +3459,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) CGRect positioningRect = [self frameOfImage:image centeredAtCoordinate:annotation.coordinate]; positioningRect.origin.x -= 0.5; + return CGRectInset(positioningRect, -MGLAnnotationImagePaddingForCallout, -MGLAnnotationImagePaddingForCallout); } @@ -4221,6 +4344,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) { [self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:(change == mbgl::MapChangeDidFinishRenderingFrameFullyRendered)]; } + [self updateAnnotationViews]; break; } } @@ -4231,6 +4355,85 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) [self updateUserLocationAnnotationViewAnimatedWithDuration:0]; } +- (void)updateAnnotationViews +{ + BOOL delegateImplementsViewForAnnotation = [self.delegate respondsToSelector:@selector(mapView:viewForAnnotation:)]; + + if (!delegateImplementsViewForAnnotation) + { + return; + } + + // Update all visible annotation views + std::set<MGLAnnotationTag> visibleTags; + std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:CGRectInset(self.bounds, -_largestAnnotationViewSize.width - MGLAnnotationUpdateViewportOutset.width, -_largestAnnotationViewSize.height - MGLAnnotationUpdateViewportOutset.width)]; + + for(auto const& annotationTag: annotationTags) + { + auto &annotationContext = _annotationContextsByAnnotationTag[annotationTag]; + id<MGLAnnotation> 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 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) 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 df08c1b4a2..3b7361a756 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" |