diff --git a/Examples/UIExplorer/ModalExample.js b/Examples/UIExplorer/ModalExample.js new file mode 100644 index 000000000..37d2cc245 --- /dev/null +++ b/Examples/UIExplorer/ModalExample.js @@ -0,0 +1,99 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Modal, + StyleSheet, + Text, + TouchableHighlight, + View, +} = React; + +exports.displayName = (undefined: ?string); +exports.framework = 'React'; +exports.title = ''; +exports.description = 'Component for presenting modal views.'; + +var ModalExample = React.createClass({ + getInitialState: function() { + return { + openModal: null, + }; + }, + + _closeModal: function() { + this.setState({openModal: null}); + }, + + _openAnimatedModal: function() { + this.setState({openModal: 'animated'}); + }, + + _openNotAnimatedModal: function() { + this.setState({openModal: 'not-animated'}); + }, + + render: function() { + return ( + + + + This modal was presented with animation. + + Close + + + + + + + This modal was presented immediately, without animation. + + Close + + + + + + Present Animated + + + + Present Without Animation + + + ); + }, +}); + +exports.examples = [ + { + title: 'Modal Presentation', + description: 'Modals can be presented with or without animation', + render: () => , + }, +]; + +var styles = StyleSheet.create({ + container: { + alignItems: 'center', + backgroundColor: '#f5fcff', + flex: 1, + justifyContent: 'center', + }, +}); diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index d2e04063b..d76a19936 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -37,6 +37,7 @@ var COMPONENTS = [ require('./ListViewGridLayoutExample'), require('./ListViewPagingExample'), require('./MapViewExample'), + require('./ModalExample'), require('./Navigator/NavigatorExample'), require('./NavigatorIOSColorsExample'), require('./NavigatorIOSExample'), diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js new file mode 100644 index 000000000..23bf730ec --- /dev/null +++ b/Libraries/Modal/Modal.js @@ -0,0 +1,48 @@ +/** + * 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. + * + * @providesModule Modal + * @flow + */ +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var requireNativeComponent = require('requireNativeComponent'); +var RCTModalHostView = requireNativeComponent('RCTModalHostView', null); + +class Modal extends React.Component { + render(): ?ReactElement { + if (this.props.visible === false) { + return null; + } + + return ( + + + {this.props.children} + + + ); + } +} + +var styles = StyleSheet.create({ + modal: { + position: 'absolute', + }, + container: { + left: 0, + position: 'absolute', + top: 0, + } +}); + +module.exports = Modal; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 84b01e8c4..7d005069f 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -24,6 +24,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { Image: require('Image'), ListView: require('ListView'), MapView: require('MapView'), + Modal: require('Modal'), Navigator: require('Navigator'), NavigatorIOS: require('NavigatorIOS'), PickerIOS: require('PickerIOS'), diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index b63dd4aaa..f66a9b8b7 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -261,7 +261,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) { super.frame = frame; if (self.reactTag && _bridge.isValid) { - [_bridge.uiManager setFrame:frame forRootView:self]; + [_bridge.uiManager setFrame:frame forView:self]; } } diff --git a/React/Modules/RCTUIManager.h b/React/Modules/RCTUIManager.h index cbd7c167f..79ce01463 100644 --- a/React/Modules/RCTUIManager.h +++ b/React/Modules/RCTUIManager.h @@ -40,10 +40,10 @@ - (UIView *)viewForReactTag:(NSNumber *)reactTag; /** - * Update the frame of a root view. This might be in response to a screen rotation + * Update the frame of a view. This might be in response to a screen rotation * or some other layout event outside of the React-managed view hierarchy. */ -- (void)setFrame:(CGRect)frame forRootView:(UIView *)rootView; +- (void)setFrame:(CGRect)frame forView:(UIView *)view; /** * Update the background color of a root view. This is usually triggered by diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index d1af57705..de25ef398 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -368,13 +368,11 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass) return _viewRegistry[reactTag]; } -- (void)setFrame:(CGRect)frame forRootView:(UIView *)rootView +- (void)setFrame:(CGRect)frame forView:(UIView *)view { RCTAssertMainThread(); - NSNumber *reactTag = rootView.reactTag; - RCTAssert(RCTIsReactRootView(reactTag), @"Specified view %@ is not a root view", reactTag); - + NSNumber *reactTag = view.reactTag; dispatch_async(_shadowQueue, ^{ RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag]; RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag); diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 74cc3e0be..bf69290a0 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -67,6 +67,9 @@ 63F014C01B02080B003B75D2 /* RCTPointAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */; }; 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; + 83392EB31B6634E10013B15F /* RCTModalHostViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 83392EB21B6634E10013B15F /* RCTModalHostViewController.m */; }; + 83A1FE8C1B62640A00BE0E65 /* RCTModalHostView.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A1FE8B1B62640A00BE0E65 /* RCTModalHostView.m */; }; + 83A1FE8F1B62643A00BE0E65 /* RCTModalHostViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A1FE8E1B62643A00BE0E65 /* RCTModalHostViewManager.m */; }; 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */; }; 83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */; }; 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA501A601E3B00E9B192 /* RCTUtils.m */; }; @@ -218,6 +221,12 @@ 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 = ""; }; + 83392EB11B6634E10013B15F /* RCTModalHostViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModalHostViewController.h; sourceTree = ""; }; + 83392EB21B6634E10013B15F /* RCTModalHostViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModalHostViewController.m; sourceTree = ""; }; + 83A1FE8A1B62640A00BE0E65 /* RCTModalHostView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModalHostView.h; sourceTree = ""; }; + 83A1FE8B1B62640A00BE0E65 /* RCTModalHostView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModalHostView.m; sourceTree = ""; }; + 83A1FE8D1B62643A00BE0E65 /* RCTModalHostViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModalHostViewManager.h; sourceTree = ""; }; + 83A1FE8E1B62643A00BE0E65 /* RCTModalHostViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModalHostViewManager.m; sourceTree = ""; }; 83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSparseArray.h; sourceTree = ""; }; 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSparseArray.m; sourceTree = ""; }; 83CBBA2E1A601D0E00E9B192 /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -317,6 +326,12 @@ 14435CE21AAC4AE100FC20F4 /* RCTMap.m */, 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */, 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */, + 83A1FE8A1B62640A00BE0E65 /* RCTModalHostView.h */, + 83A1FE8B1B62640A00BE0E65 /* RCTModalHostView.m */, + 83392EB11B6634E10013B15F /* RCTModalHostViewController.h */, + 83392EB21B6634E10013B15F /* RCTModalHostViewController.m */, + 83A1FE8D1B62643A00BE0E65 /* RCTModalHostViewManager.h */, + 83A1FE8E1B62643A00BE0E65 /* RCTModalHostViewManager.m */, 13B0800C1A69489C00A75B9A /* RCTNavigator.h */, 13B0800D1A69489C00A75B9A /* RCTNavigator.m */, 13B0800E1A69489C00A75B9A /* RCTNavigatorManager.h */, @@ -359,6 +374,7 @@ 137327E41AA5CF210034F82E /* RCTTabBarItemManager.m */, 137327E51AA5CF210034F82E /* RCTTabBarManager.h */, 137327E61AA5CF210034F82E /* RCTTabBarManager.m */, + E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */, 13E0674F1A70F44B002CDEE1 /* RCTView.h */, 13E067501A70F44B002CDEE1 /* RCTView.m */, 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */, @@ -373,7 +389,6 @@ 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */, 13E067531A70F44B002CDEE1 /* UIView+React.h */, 13E067541A70F44B002CDEE1 /* UIView+React.m */, - E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */, ); path = Views; sourceTree = ""; @@ -607,6 +622,7 @@ 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */, 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */, 137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */, + 83A1FE8C1B62640A00BE0E65 /* RCTModalHostView.m in Sources */, 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */, 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */, 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */, @@ -614,12 +630,14 @@ 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */, 00C1A2B31AC0B7E000E89A1C /* RCTDevMenu.m in Sources */, 63F014C01B02080B003B75D2 /* RCTPointAnnotation.m in Sources */, + 83392EB31B6634E10013B15F /* RCTModalHostViewController.m in Sources */, 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */, 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */, 13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */, 1385D0341B665AAE000A309B /* RCTModuleMap.m in Sources */, 1403F2B31B0AE60700C2A9A4 /* RCTPerfStats.m in Sources */, 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */, + 83A1FE8F1B62643A00BE0E65 /* RCTModalHostViewManager.m in Sources */, 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */, 138D6A141B53CD290074A87E /* RCTCache.m in Sources */, 13B0801B1A69489C00A75B9A /* RCTNavigatorManager.m in Sources */, diff --git a/React/Views/RCTModalHostView.h b/React/Views/RCTModalHostView.h new file mode 100644 index 000000000..a39df2815 --- /dev/null +++ b/React/Views/RCTModalHostView.h @@ -0,0 +1,20 @@ +/** + * 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 + +@class RCTBridge; + +@interface RCTModalHostView : UIView + +@property (nonatomic, assign, getter=isAnimated) BOOL animated; + +- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; + +@end diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m new file mode 100644 index 000000000..c12806c52 --- /dev/null +++ b/React/Views/RCTModalHostView.m @@ -0,0 +1,82 @@ +/** + * 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 "RCTModalHostView.h" + +#import "RCTAssert.h" +#import "RCTBridge.h" +#import "RCTModalHostViewController.h" +#import "RCTTouchHandler.h" +#import "RCTUIManager.h" +#import "UIView+React.h" + +@implementation RCTModalHostView +{ + RCTBridge *_bridge; + BOOL _hasModalView; + RCTModalHostViewController *_modalViewController; + RCTTouchHandler *_touchHandler; +} + +RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) + +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + if ((self = [super initWithFrame:CGRectZero])) { + _bridge = bridge; + _modalViewController = [[RCTModalHostViewController alloc] init]; + _touchHandler = [[RCTTouchHandler alloc] initWithBridge:bridge]; + + __weak RCTModalHostView *weakSelf = self; + _modalViewController.boundsDidChangeBlock = ^(CGRect newBounds) { + [weakSelf notifyForBoundsChange:newBounds]; + }; + } + + return self; +} + +- (void)notifyForBoundsChange:(CGRect)newBounds +{ + if (_hasModalView) { + [_bridge.uiManager setFrame:newBounds forView:_modalViewController.view]; + } +} + +- (NSArray *)reactSubviews +{ + return _hasModalView ? @[_modalViewController.view] : @[]; +} + +- (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex +{ + [subview addGestureRecognizer:_touchHandler]; + _modalViewController.view = subview; + _hasModalView = YES; +} + +- (void)removeReactSubview:(UIView *)subview +{ + RCTAssert(subview == _modalViewController.view, @"Cannot remove view other than modal view"); + _modalViewController.view = nil; + _hasModalView = NO; +} + +- (void)didMoveToSuperview +{ + [super didMoveToSuperview]; + + if (self.superview) { + [self.backingViewController presentViewController:_modalViewController animated:self.animated completion:nil]; + } else { + [_modalViewController dismissViewControllerAnimated:self.animated completion:nil]; + } +} + +@end diff --git a/React/Views/RCTModalHostViewController.h b/React/Views/RCTModalHostViewController.h new file mode 100644 index 000000000..ceadfbcc5 --- /dev/null +++ b/React/Views/RCTModalHostViewController.h @@ -0,0 +1,16 @@ +/** + * 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 RCTModalHostViewController : UIViewController + +@property (nonatomic, copy) void (^boundsDidChangeBlock)(CGRect newBounds); + +@end diff --git a/React/Views/RCTModalHostViewController.m b/React/Views/RCTModalHostViewController.m new file mode 100644 index 000000000..196dc6a81 --- /dev/null +++ b/React/Views/RCTModalHostViewController.m @@ -0,0 +1,27 @@ +/** + * 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 "RCTModalHostViewController.h" + +@interface RCTModalHostViewController () + +@end + +@implementation RCTModalHostViewController + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + if (self.boundsDidChangeBlock) { + self.boundsDidChangeBlock(self.view.bounds); + } +} + +@end diff --git a/React/Views/RCTModalHostViewManager.h b/React/Views/RCTModalHostViewManager.h new file mode 100644 index 000000000..e47794542 --- /dev/null +++ b/React/Views/RCTModalHostViewManager.h @@ -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 "RCTViewManager.h" + +@interface RCTModalHostViewManager : RCTViewManager + +@end diff --git a/React/Views/RCTModalHostViewManager.m b/React/Views/RCTModalHostViewManager.m new file mode 100644 index 000000000..458e65081 --- /dev/null +++ b/React/Views/RCTModalHostViewManager.m @@ -0,0 +1,27 @@ +/** + * 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 "RCTModalHostViewManager.h" + +#import "RCTBridge.h" +#import "RCTModalHostView.h" +#import "RCTTouchHandler.h" + +@implementation RCTModalHostViewManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[RCTModalHostView alloc] initWithBridge:self.bridge]; +} + +RCT_EXPORT_VIEW_PROPERTY(animated, BOOL) + +@end