diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js index 50e36954bd00a0..b69c2417a9059d 100644 --- a/Libraries/Components/MapView/MapView.js +++ b/Libraries/Components/MapView/MapView.js @@ -35,6 +35,34 @@ type MapRegion = { var MapView = React.createClass({ mixins: [NativeMethodsMixin], + checkAnnotationIds: function (annotations: Array) { + + var newAnnotations = annotations.map(function (annotation) { + if (!annotation['id']) { + // TODO: add a base64 (or similar) encoder here + annotation['id'] = encodeURIComponent(JSON.stringify(annotation)); + } + + return annotation; + }); + + this.setState({ + annotations: newAnnotations + }); + }, + + componentWillMount: function() { + if (this.props.annotations) { + this.checkAnnotationIds(this.props.annotations); + } + }, + + componentWillReceiveProps: function(nextProps: Object) { + if (nextProps.annotations) { + this.checkAnnotationIds(nextProps.annotations); + } + }, + propTypes: { /** * Used to style and layout the `MapView`. See `StyleSheet.js` and @@ -126,11 +154,34 @@ var MapView = React.createClass({ latitude: React.PropTypes.number.isRequired, longitude: React.PropTypes.number.isRequired, + /** + * Whether the pin drop should be animated or not + */ + animateDrop: React.PropTypes.bool, + /** * Annotation title/subtile. */ title: React.PropTypes.string, subtitle: React.PropTypes.string, + + /** + * Whether the Annotation has callout buttons. + */ + hasLeftCallout: React.PropTypes.bool, + hasRightCallout: React.PropTypes.bool, + + /** + * Event handlers for callout buttons. + */ + onLeftCalloutPress: React.PropTypes.func, + onRightCalloutPress: React.PropTypes.func, + + /** + * annotation id + */ + id: React.PropTypes.string + })), /** @@ -158,6 +209,11 @@ var MapView = React.createClass({ * Callback that is called once, when the user is done moving the map. */ onRegionChangeComplete: React.PropTypes.func, + + /** + * Callback that is called once, when the user is clicked on a annotation. + */ + onAnnotationPress: React.PropTypes.func, }, _onChange: function(event: Event) { @@ -170,8 +226,33 @@ var MapView = React.createClass({ } }, + _onPress: function(event: Event) { + if (event.nativeEvent.action === "annotation-click") + this.props.onAnnotationPress && this.props.onAnnotationPress(event.nativeEvent.annotation); + + if (event.nativeEvent.action === "callout-click") { + if (!this.props.annotations) { + return; + } + + // Find the annotation with the id of what has been pressed + for (var i = 0; i < this.props.annotations.length; i++) { + var annotation = this.props.annotations[i]; + if (annotation['id'] === event.nativeEvent.annotationId) { + // Pass the right function + if (event.nativeEvent.side === "left") + annotation.onLeftCalloutPress && annotation.onLeftCalloutPress(event.nativeEvent); + + if (event.nativeEvent.side === "right") + annotation.onRightCalloutPress && annotation.onRightCalloutPress(event.nativeEvent); + } + } + + } + }, + render: function() { - return ; + return ; }, }); diff --git a/React/Modules/RCTPointAnnotation.h b/React/Modules/RCTPointAnnotation.h new file mode 100644 index 00000000000000..4c6ae793b87ba9 --- /dev/null +++ b/React/Modules/RCTPointAnnotation.h @@ -0,0 +1,18 @@ +// +// RCTPointAnnotation.h +// React +// +// Created by David Mohl on 5/12/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +@interface RCTPointAnnotation : MKPointAnnotation + +@property NSString *identifier; +@property BOOL hasLeftCallout; +@property BOOL hasRightCallout; +@property BOOL animateDrop; + +@end diff --git a/React/Modules/RCTPointAnnotation.m b/React/Modules/RCTPointAnnotation.m new file mode 100644 index 00000000000000..53fb673a3f9777 --- /dev/null +++ b/React/Modules/RCTPointAnnotation.m @@ -0,0 +1,13 @@ +// +// RCTPointAnnotation.m +// React +// +// Created by David Mohl on 5/12/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTPointAnnotation.h" + +@implementation RCTPointAnnotation + +@end diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 513773b6d536f3..bd9e7a4c08d088 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; }; 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; }; 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */; }; + 63F014C01B02080B003B75D2 /* RCTPointAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */; }; 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 830BA4541A8E3BDA00D53203 /* RCTCache.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; @@ -200,6 +201,8 @@ 58114A4F1AAE93D500E7D092 /* RCTAsyncLocalStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAsyncLocalStorage.h; sourceTree = ""; }; 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDatePickerManager.m; sourceTree = ""; }; 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDatePickerManager.h; sourceTree = ""; }; + 63F014BE1B02080B003B75D2 /* RCTPointAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointAnnotation.h; sourceTree = ""; }; + 63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPointAnnotation.m; sourceTree = ""; }; 830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = ""; }; 830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = ""; }; 830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = ""; }; @@ -278,6 +281,8 @@ 13B07FEE1A69327A00A75B9A /* RCTTiming.m */, 13E067481A70F434002CDEE1 /* RCTUIManager.h */, 13E067491A70F434002CDEE1 /* RCTUIManager.m */, + 63F014BE1B02080B003B75D2 /* RCTPointAnnotation.h */, + 63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */, ); path = Modules; sourceTree = ""; @@ -573,6 +578,7 @@ 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */, 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */, 00C1A2B31AC0B7E000E89A1C /* RCTDevMenu.m in Sources */, + 63F014C01B02080B003B75D2 /* RCTPointAnnotation.m in Sources */, 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */, 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */, 13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */, diff --git a/React/Views/RCTConvert+MapKit.h b/React/Views/RCTConvert+MapKit.h index d4bf8d2d765fb9..04cf77018f8db2 100644 --- a/React/Views/RCTConvert+MapKit.h +++ b/React/Views/RCTConvert+MapKit.h @@ -7,7 +7,7 @@ // #import - +#import "RCTPointAnnotation.h" #import "RCTConvert.h" @interface RCTConvert (MapKit) @@ -16,8 +16,12 @@ + (MKCoordinateRegion)MKCoordinateRegion:(id)json; + (MKShape *)MKShape:(id)json; + (MKMapType)MKMapType:(id)json; ++ (RCTPointAnnotation *)RCTPointAnnotation:(id)json; typedef NSArray MKShapeArray; + (MKShapeArray *)MKShapeArray:(id)json; +typedef NSArray RCTPointAnnotationArray; ++ (RCTPointAnnotationArray *)RCTPointAnnotationArray:(id)json; + @end diff --git a/React/Views/RCTConvert+MapKit.m b/React/Views/RCTConvert+MapKit.m index 6dc541a460931a..6ff8b934e33bc2 100644 --- a/React/Views/RCTConvert+MapKit.m +++ b/React/Views/RCTConvert+MapKit.m @@ -7,8 +7,8 @@ // #import "RCTConvert+MapKit.h" - #import "RCTConvert+CoreLocation.h" +#import "RCTPointAnnotation.h" @implementation RCTConvert(MapKit) @@ -49,4 +49,20 @@ + (MKShape *)MKShape:(id)json @"hybrid": @(MKMapTypeHybrid), }), MKMapTypeStandard, integerValue) ++ (RCTPointAnnotation *)RCTPointAnnotation:(id)json +{ + json = [self NSDictionary:json]; + RCTPointAnnotation *shape = [[RCTPointAnnotation alloc] init]; + shape.coordinate = [self CLLocationCoordinate2D:json]; + shape.title = [RCTConvert NSString:json[@"title"]]; + shape.subtitle = [RCTConvert NSString:json[@"subtitle"]]; + shape.identifier = [RCTConvert NSString:json[@"id"]]; + shape.hasLeftCallout = [RCTConvert BOOL:json[@"hasLeftCallout"]]; + shape.hasRightCallout = [RCTConvert BOOL:json[@"hasRightCallout"]]; + shape.animateDrop = [RCTConvert BOOL:json[@"animateDrop"]]; + return shape; +} + +RCT_ARRAY_CONVERTER(RCTPointAnnotation) + @end diff --git a/React/Views/RCTMap.h b/React/Views/RCTMap.h index d372db56e465d9..41cc13a12e56b2 100644 --- a/React/Views/RCTMap.h +++ b/React/Views/RCTMap.h @@ -26,7 +26,8 @@ extern const CGFloat RCTMapZoomBoundBuffer; @property (nonatomic, assign) CGFloat maxDelta; @property (nonatomic, assign) UIEdgeInsets legalLabelInsets; @property (nonatomic, strong) NSTimer *regionChangeObserveTimer; +@property (nonatomic, strong) NSMutableArray *annotationIds; -- (void)setAnnotations:(MKShapeArray *)annotations; +- (void)setAnnotations:(RCTPointAnnotationArray *)annotations; @end diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 40b60508e26da4..03d0ef76ea8fda 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -26,9 +26,9 @@ @implementation RCTMap - (instancetype)init { if ((self = [super init])) { - + _hasStartedRendering = NO; - + // Find Apple link label for (UIView *subview in self.subviews) { if ([NSStringFromClass(subview.class) isEqualToString:@"MKAttributionLabel"]) { @@ -55,7 +55,7 @@ - (void)reactSetFrame:(CGRect)frame - (void)layoutSubviews { [super layoutSubviews]; - + if (_legalLabel) { dispatch_async(dispatch_get_main_queue(), ^{ CGRect frame = _legalLabel.frame; @@ -86,7 +86,7 @@ - (void)setShowsUserLocation:(BOOL)showsUserLocation } } super.showsUserLocation = showsUserLocation; - + // If it needs to show user location, force map view centered // on user's current location on user location updates _followUserLocation = showsUserLocation; @@ -99,7 +99,7 @@ - (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated if (!CLLocationCoordinate2DIsValid(region.center)) { return; } - + // If new span values are nil, use old values instead if (!region.span.latitudeDelta) { region.span.latitudeDelta = self.region.span.latitudeDelta; @@ -107,17 +107,57 @@ - (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated if (!region.span.longitudeDelta) { region.span.longitudeDelta = self.region.span.longitudeDelta; } - + // Animate to new position [super setRegion:region animated:animated]; } -- (void)setAnnotations:(MKShapeArray *)annotations +- (void)setAnnotations:(RCTPointAnnotationArray *)annotations { - [self removeAnnotations:self.annotations]; - if (annotations.count) { - [self addAnnotations:annotations]; + NSMutableArray *newAnnotationIds = [[NSMutableArray alloc] init]; + NSMutableArray *annotationsToDelete = [[NSMutableArray alloc] init]; + NSMutableArray *annotationsToAdd = [[NSMutableArray alloc] init]; + + for (RCTPointAnnotation *annotation in annotations) { + if (![annotation isKindOfClass:[RCTPointAnnotation class]]) { + continue; + } + + [newAnnotationIds addObject:annotation.identifier]; + + // If the current set does not contain the new annotation, mark it as add + if (![self.annotationIds containsObject:annotation.identifier]) { + [annotationsToAdd addObject:annotation]; + } + } + + for (RCTPointAnnotation *annotation in self.annotations) { + if (![annotation isKindOfClass:[RCTPointAnnotation class]]) { + continue; + } + + // If the new set does not contain an existing annotation, mark it as delete + if (![newAnnotationIds containsObject:annotation.identifier]) { + [annotationsToDelete addObject:annotation]; + } + } + + if (annotationsToDelete.count) { + [self removeAnnotations:annotationsToDelete]; + } + + if (annotationsToAdd.count) { + [self addAnnotations:annotationsToAdd]; + } + + NSMutableArray *newIds = [[NSMutableArray alloc] init]; + for (RCTPointAnnotation *anno in self.annotations) { + if ([anno isKindOfClass:[MKUserLocation class]]) { + continue; + } + [newIds addObject:anno.identifier]; } + self.annotationIds = newIds; } @end diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m index 7a9401fdd8bae1..6411004cc54420 100644 --- a/React/Views/RCTMapManager.m +++ b/React/Views/RCTMapManager.m @@ -15,6 +15,9 @@ #import "RCTEventDispatcher.h" #import "RCTMap.h" #import "UIView+React.h" +#import "RCTPointAnnotation.h" + +@import MapKit; static NSString *const RCTMapViewKey = @"MapView"; @@ -42,7 +45,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(minDelta, CGFloat) RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(mapType, MKMapType) -RCT_EXPORT_VIEW_PROPERTY(annotations, MKShapeArray) +RCT_EXPORT_VIEW_PROPERTY(annotations, RCTPointAnnotationArray) RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) { [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES]; @@ -50,6 +53,73 @@ - (UIView *)view #pragma mark MKMapViewDelegate + + +- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view +{ + if (![view.annotation isKindOfClass:[MKUserLocation class]]) { + + RCTPointAnnotation *annotation = (RCTPointAnnotation *)view.annotation; + NSString *title = view.annotation.title ?: @""; + NSString *subtitle = view.annotation.subtitle ?: @""; + + NSDictionary *event = @{ + @"target": mapView.reactTag, + @"action": @"annotation-click", + @"annotation": @{ + @"id": annotation.identifier, + @"title": title, + @"subtitle": subtitle, + @"latitude": @(annotation.coordinate.latitude), + @"longitude": @(annotation.coordinate.longitude) + } + }; + + [self.bridge.eventDispatcher sendInputEventWithName:@"topTap" body:event]; + } +} + +- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(RCTPointAnnotation *)annotation +{ + if ([annotation isKindOfClass:[MKUserLocation class]]) { + return nil; + } + + MKPinAnnotationView *annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"RCTAnnotation"]; + + annotationView.canShowCallout = true; + annotationView.animatesDrop = annotation.animateDrop; + + annotationView.leftCalloutAccessoryView = nil; + if (annotation.hasLeftCallout) { + annotationView.leftCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure]; + } + + annotationView.rightCalloutAccessoryView = nil; + if (annotation.hasRightCallout) { + annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure]; + } + + return annotationView; +} + +- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control +{ + // Pass to js + RCTPointAnnotation *annotation = (RCTPointAnnotation *)view.annotation; + NSString *side = (control == view.leftCalloutAccessoryView) ? @"left" : @"right"; + + NSDictionary *event = @{ + @"target": mapView.reactTag, + @"side": side, + @"action": @"callout-click", + @"annotationId": annotation.identifier + }; + + [self.bridge.eventDispatcher sendInputEventWithName:@"topTap" body:event]; +} + + - (void)mapView:(RCTMap *)mapView didUpdateUserLocation:(MKUserLocation *)location { if (mapView.followUserLocation) { @@ -143,7 +213,7 @@ - (void)_emitRegionChangeEvent:(RCTMap *)mapView continuous:(BOOL)continuous #define FLUSH_NAN(value) (isnan(value) ? 0 : value) NSDictionary *event = @{ - @"target": [mapView reactTag], + @"target": mapView.reactTag, @"continuous": @(continuous), @"region": @{ @"latitude": @(FLUSH_NAN(region.center.latitude)),