diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js index 50e36954bd00a0..ea10b959e82ad9 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 @@ -84,14 +112,14 @@ var MapView = React.createClass({ /** * The map type to be displayed. - * + * * - standard: standard road map (default) * - satellite: satellite view * - hybrid: satellite view with roads and points of interest overlayed */ mapType: React.PropTypes.oneOf([ - 'standard', - 'satellite', + 'standard', + 'satellite', 'hybrid', ]), @@ -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,34 @@ 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); + } else 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..0646608d4805bc --- /dev/null +++ b/React/Modules/RCTPointAnnotation.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface RCTPointAnnotation : MKPointAnnotation + +@property (nonatomic, copy) NSString *identifier; +@property (nonatomic, assign) BOOL hasLeftCallout; +@property (nonatomic, assign) BOOL hasRightCallout; +@property (nonatomic, assign) BOOL animateDrop; + +@end diff --git a/React/Modules/RCTPointAnnotation.m b/React/Modules/RCTPointAnnotation.m new file mode 100644 index 00000000000000..aaaf2d7e20a254 --- /dev/null +++ b/React/Modules/RCTPointAnnotation.m @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTPointAnnotation.h" + +@implementation RCTPointAnnotation + +@end diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 51f02e7b2204e9..099c120a03a4fc 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -64,6 +64,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 */; }; @@ -213,6 +214,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 = ""; }; @@ -291,6 +294,8 @@ 13B07FEE1A69327A00A75B9A /* RCTTiming.m */, 13E067481A70F434002CDEE1 /* RCTUIManager.h */, 13E067491A70F434002CDEE1 /* RCTUIManager.m */, + 63F014BE1B02080B003B75D2 /* RCTPointAnnotation.h */, + 63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */, ); path = Modules; sourceTree = ""; @@ -599,6 +604,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+CoreLocation.h b/React/Views/RCTConvert+CoreLocation.h index 89e0c729c33361..e8c1e73853b307 100644 --- a/React/Views/RCTConvert+CoreLocation.h +++ b/React/Views/RCTConvert+CoreLocation.h @@ -1,10 +1,11 @@ -// -// RCTConvert+CoreLocation.h -// React -// -// Created by Nick Lockwood on 12/04/2015. -// Copyright (c) 2015 Facebook. All rights reserved. -// +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ #import diff --git a/React/Views/RCTConvert+CoreLocation.m b/React/Views/RCTConvert+CoreLocation.m index a347c7fea750d3..505a6aba3f3368 100644 --- a/React/Views/RCTConvert+CoreLocation.m +++ b/React/Views/RCTConvert+CoreLocation.m @@ -1,10 +1,11 @@ -// -// RCTConvert+CoreLocation.m -// React -// -// Created by Nick Lockwood on 12/04/2015. -// Copyright (c) 2015 Facebook. All rights reserved. -// +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ #import "RCTConvert+CoreLocation.h" diff --git a/React/Views/RCTConvert+MapKit.h b/React/Views/RCTConvert+MapKit.h index d4bf8d2d765fb9..d3e7fbc153f3e7 100644 --- a/React/Views/RCTConvert+MapKit.h +++ b/React/Views/RCTConvert+MapKit.h @@ -1,13 +1,15 @@ -// -// RCTConvert+MapKit.h -// React -// -// Created by Nick Lockwood on 12/04/2015. -// Copyright (c) 2015 Facebook. All rights reserved. -// +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ #import +#import "RCTPointAnnotation.h" #import "RCTConvert.h" @interface RCTConvert (MapKit) @@ -16,8 +18,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..a1ba0ac98efa65 100644 --- a/React/Views/RCTConvert+MapKit.m +++ b/React/Views/RCTConvert+MapKit.m @@ -1,14 +1,15 @@ -// -// RCTConvert+MapKit.m -// React -// -// Created by Nick Lockwood on 12/04/2015. -// Copyright (c) 2015 Facebook. All rights reserved. -// +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ #import "RCTConvert+MapKit.h" - #import "RCTConvert+CoreLocation.h" +#import "RCTPointAnnotation.h" @implementation RCTConvert(MapKit) @@ -49,4 +50,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..d51af5a01ddb00 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -112,12 +112,52 @@ - (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated [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 b1c5c84b836163..fba2a60fab7b7a 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 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)), diff --git a/React/Views/RCTSegmentedControl.h b/React/Views/RCTSegmentedControl.h index 8e6e1255ef0492..3e95735bd31f75 100644 --- a/React/Views/RCTSegmentedControl.h +++ b/React/Views/RCTSegmentedControl.h @@ -1,10 +1,11 @@ -// -// RCTSegmentedControl.h -// React -// -// Created by Clay Allsopp on 3/31/15. -// Copyright (c) 2015 Facebook. All rights reserved. -// +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ #import diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m index 59e4cfb86b5fa4..58e5629937a562 100644 --- a/React/Views/RCTSegmentedControl.m +++ b/React/Views/RCTSegmentedControl.m @@ -1,10 +1,11 @@ -// -// RCTSegmentedControl.m -// React -// -// Created by Clay Allsopp on 3/31/15. -// Copyright (c) 2015 Facebook. All rights reserved. -// +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ #import "RCTSegmentedControl.h"