diff --git a/Examples/UIExplorer/NavigatorIOSColorsExample.js b/Examples/UIExplorer/NavigatorIOSColorsExample.js index 0f695d956a..7411869026 100644 --- a/Examples/UIExplorer/NavigatorIOSColorsExample.js +++ b/Examples/UIExplorer/NavigatorIOSColorsExample.js @@ -66,6 +66,7 @@ var NavigatorIOSColors = React.createClass({ tintColor="#FFFFFF" barTintColor="#183E63" titleTextColor="#FFFFFF" + translucent="true" /> ); }, diff --git a/Libraries/Animation/LayoutAnimation.js b/Libraries/Animation/LayoutAnimation.js index c297123ba2..1a31f98d48 100644 --- a/Libraries/Animation/LayoutAnimation.js +++ b/Libraries/Animation/LayoutAnimation.js @@ -23,6 +23,7 @@ var TypesEnum = { easeInEaseOut: true, easeIn: true, easeOut: true, + keyboard: true, }; var Types = keyMirror(TypesEnum); @@ -113,4 +114,10 @@ var LayoutAnimation = { } }; +for (var key in LayoutAnimation.Presets) { + LayoutAnimation[key] = LayoutAnimation.configureNext.bind( + null, LayoutAnimation.Presets[key] + ); +} + module.exports = LayoutAnimation; diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js index 50e36954bd..da4350e4c2 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 ; }, }); @@ -179,6 +261,7 @@ if (Platform.OS === 'android') { var RCTMap = createReactNativeComponentClass({ validAttributes: merge( ReactNativeViewAttributes.UIView, { + active: true, showsUserLocation: true, zoomEnabled: true, rotateEnabled: true, diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index 9ecf6bdc80..788273ec53 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -57,6 +57,7 @@ var RCTNavigatorItem = createReactNativeComponentClass({ backButtonIcon: true, backButtonTitle: true, tintColor: true, + translucent: true, navigationBarHidden: true, titleTextColor: true, style: true, @@ -300,6 +301,11 @@ var NavigatorIOS = React.createClass({ */ titleTextColor: PropTypes.string, + /** + * A Boolean value that indicates whether the navigation bar is translucent + */ + translucent: PropTypes.bool, + }, navigator: (undefined: ?Object), @@ -609,6 +615,7 @@ var NavigatorIOS = React.createClass({ navigationBarHidden={this.props.navigationBarHidden} tintColor={this.props.tintColor} barTintColor={this.props.barTintColor} + translucent={this.props.translucent} titleTextColor={this.props.titleTextColor}> { + this._measureAndUpdateScrollProps(); + }); }, onRowHighlighted: function(sectionID, rowID) { diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index d9e452d22d..7b7bf1ae62 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -30,6 +30,7 @@ var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule; var Dimensions = require('Dimensions'); var InteractionMixin = require('InteractionMixin'); +var Map = require('Map'); var NavigationContext = require('NavigationContext'); var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar'); var NavigatorNavigationBar = require('NavigatorNavigationBar'); @@ -257,6 +258,8 @@ var Navigator = React.createClass({ }, getInitialState: function() { + this._renderedSceneMap = new Map(); + var routeStack = this.props.initialRouteStack || [this.props.initialRoute]; invariant( routeStack.length >= 1, @@ -276,10 +279,6 @@ var Navigator = React.createClass({ ), idStack: routeStack.map(() => getuid()), routeStack, - // `updatingRange*` allows us to only render the visible or staged scenes - // On first render, we will render every scene in the initialRouteStack - updatingRangeStart: 0, - updatingRangeLength: routeStack.length, presentedIndex: initialRouteIndex, transitionFromIndex: null, activeGesture: null, @@ -351,8 +350,6 @@ var Navigator = React.createClass({ sceneConfigStack: nextRouteStack.map( this.props.configureScene ), - updatingRangeStart: 0, - updatingRangeLength: nextRouteStack.length, presentedIndex: destIndex, activeGesture: null, transitionFromIndex: null, @@ -395,7 +392,6 @@ var Navigator = React.createClass({ this.spring.getSpringConfig().tension = sceneConfig.springTension; this.spring.setVelocity(velocity || sceneConfig.defaultTransitionVelocity); this.spring.setEndValue(1); - this._emitWillFocus(this.state.routeStack[this.state.presentedIndex]); }, /** @@ -461,6 +457,7 @@ var Navigator = React.createClass({ if (this.state.transitionQueue.length) { var queuedTransition = this.state.transitionQueue.shift(); this._enableScene(queuedTransition.destIndex); + this._emitWillFocus(this.state.routeStack[queuedTransition.destIndex]); this._transitionTo( queuedTransition.destIndex, queuedTransition.velocity, @@ -660,6 +657,7 @@ var Navigator = React.createClass({ } } else { // The gesture has enough velocity to complete, so we transition to the gesture's destination + this._emitWillFocus(this.state.routeStack[destIndex]); this._transitionTo( destIndex, transitionVelocity, @@ -829,11 +827,6 @@ var Navigator = React.createClass({ return false; }, - _resetUpdatingRange: function() { - this.state.updatingRangeStart = 0; - this.state.updatingRangeLength = this.state.routeStack.length; - }, - _getDestIndexWithinBounds: function(n) { var currentIndex = this.state.presentedIndex; var destIndex = currentIndex + n; @@ -851,15 +844,9 @@ var Navigator = React.createClass({ _jumpN: function(n) { var destIndex = this._getDestIndexWithinBounds(n); - var requestTransitionAndResetUpdatingRange = () => { - this._enableScene(destIndex); - this._transitionTo(destIndex); - this._resetUpdatingRange(); - }; - this.setState({ - updatingRangeStart: destIndex, - updatingRangeLength: 1, - }, requestTransitionAndResetUpdatingRange); + this._enableScene(destIndex); + this._emitWillFocus(this.state.routeStack[destIndex]); + this._transitionTo(destIndex); }, jumpTo: function(route) { @@ -891,18 +878,15 @@ var Navigator = React.createClass({ var nextAnimationConfigStack = activeAnimationConfigStack.concat([ this.props.configureScene(route), ]); - var requestTransitionAndResetUpdatingRange = () => { - this._enableScene(destIndex); - this._transitionTo(destIndex); - this._resetUpdatingRange(); - }; + this._emitWillFocus(nextStack[destIndex]); this.setState({ idStack: nextIDStack, routeStack: nextStack, sceneConfigStack: nextAnimationConfigStack, - updatingRangeStart: nextStack.length - 1, - updatingRangeLength: 1, - }, requestTransitionAndResetUpdatingRange); + }, () => { + this._enableScene(destIndex); + this._transitionTo(destIndex); + }); }, _popN: function(n) { @@ -915,6 +899,7 @@ var Navigator = React.createClass({ ); var popIndex = this.state.presentedIndex - n; this._enableScene(popIndex); + this._emitWillFocus(this.state.routeStack[popIndex]); this._transitionTo( popIndex, null, // default velocity @@ -954,16 +939,15 @@ var Navigator = React.createClass({ nextRouteStack[index] = route; nextAnimationModeStack[index] = this.props.configureScene(route); + if (index === this.state.presentedIndex) { + this._emitWillFocus(route); + } this.setState({ idStack: nextIDStack, routeStack: nextRouteStack, sceneConfigStack: nextAnimationModeStack, - updatingRangeStart: index, - updatingRangeLength: 1, }, () => { - this._resetUpdatingRange(); if (index === this.state.presentedIndex) { - this._emitWillFocus(route); this._emitDidFocus(route); } cb && cb(); @@ -1034,69 +1018,17 @@ var Navigator = React.createClass({ var newStackLength = index + 1; // Remove any unneeded rendered routes. if (newStackLength < this.state.routeStack.length) { - var updatingRangeStart = newStackLength; // One past the top - var updatingRangeLength = this.state.routeStack.length - newStackLength + 1; this.state.idStack.slice(newStackLength).map((removingId) => { this._itemRefs[removingId] = null; }); this.setState({ - updatingRangeStart: updatingRangeStart, - updatingRangeLength: updatingRangeLength, sceneConfigStack: this.state.sceneConfigStack.slice(0, newStackLength), idStack: this.state.idStack.slice(0, newStackLength), routeStack: this.state.routeStack.slice(0, newStackLength), - }, this._resetUpdatingRange); + }); } }, - _renderOptimizedScenes: function() { - // To avoid rendering scenes that are not visible, we use - // updatingRangeStart and updatingRangeLength to track the scenes that need - // to be updated. - - // To avoid visual glitches, we never re-render scenes during a transition. - // We assume that `state.updatingRangeLength` will have a length during the - // initial render of any scene - var shouldRenderScenes = this.state.updatingRangeLength !== 0; - if (shouldRenderScenes) { - return ( - - - {this.state.routeStack.map(this._renderOptimizedScene)} - - - ); - } - // If no scenes are changing, we can save render time. React will notice - // that we are rendering a StaticContainer in the same place, so the - // existing element will be updated. When React asks the element - // shouldComponentUpdate, the StaticContainer will return false, and the - // children from the previous reconciliation will remain. - return ( - - ); - }, - - _renderOptimizedScene: function(route, i) { - var shouldRenderScene = - i >= this.state.updatingRangeStart && - i <= this.state.updatingRangeStart + this.state.updatingRangeLength; - var scene = shouldRenderScene ? this._renderScene(route, i) : null; - return ( - - {scene} - - ); - }, - _renderScene: function(route, i) { var child = this.props.renderScene( route, @@ -1146,9 +1078,30 @@ var Navigator = React.createClass({ }, render: function() { + var newRenderedSceneMap = new Map(); + var scenes = this.state.routeStack.map((route, index) => { + var renderedScene; + if (this._renderedSceneMap.has(route) && + index !== this.state.presentedIndex) { + renderedScene = this._renderedSceneMap.get(route); + } else { + renderedScene = this._renderScene(route, index); + } + newRenderedSceneMap.set(route, renderedScene); + return renderedScene; + }); + this._renderedSceneMap = newRenderedSceneMap; return ( - {this._renderOptimizedScenes()} + + {scenes} + {this._renderNavigationBar()} ); diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 589ee5e9ad..660d098765 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -76,15 +76,17 @@ class MessageQueue { * Public APIs */ processBatch(batch) { - ReactUpdates.batchedUpdates(() => { - batch.forEach((call) => { - let method = call.method === 'callFunctionReturnFlushedQueue' ? - '__callFunction' : '__invokeCallback'; - guard(() => this[method].apply(this, call.args)); + guard(() => { + ReactUpdates.batchedUpdates(() => { + batch.forEach((call) => { + let method = call.method === 'callFunctionReturnFlushedQueue' ? + '__callFunction' : '__invokeCallback'; + guard(() => this[method].apply(this, call.args)); + }); + BridgeProfiling.profile('ReactUpdates.batchedUpdates()'); }); - BridgeProfiling.profile('ReactUpdates.batchedUpdates()'); + BridgeProfiling.profileEnd(); }); - BridgeProfiling.profileEnd(); return this.flushedQueue(); } diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 8461db3e98..6347bcc01b 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -997,6 +997,7 @@ RCT_ENUM_CONVERTER(RCTAnimationType, (@{ @"easeIn": @(RCTAnimationTypeEaseIn), @"easeOut": @(RCTAnimationTypeEaseOut), @"easeInEaseOut": @(RCTAnimationTypeEaseInEaseOut), + @"keyboard": @(RCTAnimationTypeKeyboard), }), RCTAnimationTypeEaseInEaseOut, integerValue) @end diff --git a/React/Modules/RCTPointAnnotation.h b/React/Modules/RCTPointAnnotation.h new file mode 100644 index 0000000000..0646608d48 --- /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 0000000000..aaaf2d7e20 --- /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/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index c6855fbf0d..ef2d367a08 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -58,20 +58,23 @@ static void RCTTraverseViewNodes(id view, react_view_node_b @implementation RCTAnimation -static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType type) +static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnimationType type) { switch (type) { case RCTAnimationTypeLinear: - return UIViewAnimationCurveLinear; + return UIViewAnimationOptionCurveLinear; case RCTAnimationTypeEaseIn: - return UIViewAnimationCurveEaseIn; + return UIViewAnimationOptionCurveEaseIn; case RCTAnimationTypeEaseOut: - return UIViewAnimationCurveEaseOut; + return UIViewAnimationOptionCurveEaseOut; case RCTAnimationTypeEaseInEaseOut: - return UIViewAnimationCurveEaseInOut; + return UIViewAnimationOptionCurveEaseInOut; + case RCTAnimationTypeKeyboard: + // http://stackoverflow.com/questions/18870447/how-to-use-the-default-ios7-uianimation-curve + return (UIViewAnimationOptions)(7 << 16); default: RCTLogError(@"Unsupported animation type %zd", type); - return UIViewAnimationCurveEaseInOut; + return UIViewAnimationOptionCurveEaseInOut; } } @@ -123,7 +126,7 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio } else { UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState | - UIViewAnimationCurveFromRCTAnimationType(_animationType); + UIViewAnimationOptionsFromRCTAnimationType(_animationType); [UIView animateWithDuration:_duration delay:_delay diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 51f02e7b22..099c120a03 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/RCTAnimationType.h b/React/Views/RCTAnimationType.h index 1426b54cbc..0f2703ea6b 100644 --- a/React/Views/RCTAnimationType.h +++ b/React/Views/RCTAnimationType.h @@ -15,4 +15,5 @@ typedef NS_ENUM(NSInteger, RCTAnimationType) { RCTAnimationTypeEaseIn, RCTAnimationTypeEaseOut, RCTAnimationTypeEaseInEaseOut, + RCTAnimationTypeKeyboard, }; diff --git a/React/Views/RCTConvert+CoreLocation.h b/React/Views/RCTConvert+CoreLocation.h index 89e0c729c3..e8c1e73853 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 a347c7fea7..505a6aba3f 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 d4bf8d2d76..d3e7fbc153 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 6dc541a460..a1ba0ac98e 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 @@ RCT_ENUM_CONVERTER(MKMapType, (@{ @"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 d372db56e4..41cc13a12e 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 40b60508e2..d51af5a01d 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -112,12 +112,52 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; [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 b1c5c84b83..fba2a60fab 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 @@ RCT_EXPORT_VIEW_PROPERTY(maxDelta, CGFloat) 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 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) #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 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) #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/RCTNavItem.h b/React/Views/RCTNavItem.h index cd9833a447..16c1e7f5db 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -22,6 +22,7 @@ @property (nonatomic, strong) UIColor *tintColor; @property (nonatomic, strong) UIColor *barTintColor; @property (nonatomic, strong) UIColor *titleTextColor; +@property (nonatomic, assign) BOOL translucent; @property (nonatomic, readonly) UIBarButtonItem *backButtonItem; @property (nonatomic, readonly) UIBarButtonItem *leftButtonItem; diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index 33588c938a..dab6ee049a 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -24,6 +24,7 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL) RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL) RCT_EXPORT_VIEW_PROPERTY(title, NSString) RCT_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor) diff --git a/React/Views/RCTSegmentedControl.h b/React/Views/RCTSegmentedControl.h index 8e6e1255ef..3e95735bd3 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 59e4cfb86b..58e5629937 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" diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index c8da5e4485..c1893353b1 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -81,6 +81,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) UINavigationBar *bar = self.navigationController.navigationBar; bar.barTintColor = _navItem.barTintColor; bar.tintColor = _navItem.tintColor; + bar.translucent = _navItem.translucent; if (_navItem.titleTextColor) { [bar setTitleTextAttributes:@{NSForegroundColorAttributeName : _navItem.titleTextColor}]; }