From faf913056097e4c8cc9b2e35d75fe56573a4395e Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Mon, 8 Jul 2019 03:48:26 -0700 Subject: [PATCH] fabric: Migrate ModalHostView Summary: Implements `ModalHostView` in Fabric. Reviewed By: shergin Differential Revision: D16090949 fbshipit-source-id: fe2c87063863e7f3c6c8dcc934a2cdb0b1aedaac --- .../Modal/RNModalHostViewComponentView.h | 15 ++ .../Modal/RNModalHostViewComponentView.mm | 207 ++++++++++++++++++ .../Modal/RNModalHostViewController.h | 22 ++ .../Modal/RNModalHostViewController.mm | 94 ++++++++ .../Mounting/RCTComponentViewFactory.mm | 2 + .../modal/ModalHostViewComponentDescriptor.h | 4 + 6 files changed, 344 insertions(+) create mode 100644 React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewComponentView.h create mode 100644 React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewComponentView.mm create mode 100644 React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewController.h create mode 100644 React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewController.mm diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewComponentView.h b/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewComponentView.h new file mode 100644 index 00000000000..e342a80efe6 --- /dev/null +++ b/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewComponentView.h @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** + * UIView class for root component. + */ +@interface RNModalHostViewComponentView : RCTViewComponentView + +@end diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewComponentView.mm new file mode 100644 index 00000000000..2fc69711bcc --- /dev/null +++ b/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewComponentView.mm @@ -0,0 +1,207 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNModalHostViewComponentView.h" +#import "RNModalHostViewController.h" + +#import +#import +#import +#import +#import +#import "RCTConversions.h" + +using namespace facebook::react; + +#if !TARGET_OS_TV +static UIInterfaceOrientationMask supportedOrientationsMask(ModalHostViewSupportedOrientationsMask mask) +{ + UIInterfaceOrientationMask supportedOrientations = 0; + + if (mask & ModalHostViewSupportedOrientations::Portrait) { + supportedOrientations |= UIInterfaceOrientationMaskPortrait; + } + + if (mask & ModalHostViewSupportedOrientations::PortraitUpsideDown) { + supportedOrientations |= UIInterfaceOrientationMaskPortraitUpsideDown; + } + + if (mask & ModalHostViewSupportedOrientations::Landscape) { + supportedOrientations |= UIInterfaceOrientationMaskLandscape; + } + + if (mask & ModalHostViewSupportedOrientations::LandscapeLeft) { + supportedOrientations |= UIInterfaceOrientationMaskLandscapeLeft; + } + + if (mask & ModalHostViewSupportedOrientations::LandscapeRight) { + supportedOrientations |= UIInterfaceOrientationMaskLandscapeRight; + } + + if (supportedOrientations == 0) { + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + return UIInterfaceOrientationMaskAll; + } else { + return UIInterfaceOrientationMaskPortrait; + } + } + + return supportedOrientations; +} +#endif + +static std::tuple animationConfiguration(ModalHostViewAnimationType const animation) +{ + switch (animation) { + case ModalHostViewAnimationType::None: + return std::make_tuple(NO, UIModalTransitionStyleCoverVertical); + case ModalHostViewAnimationType::Slide: + return std::make_tuple(YES, UIModalTransitionStyleCoverVertical); + case ModalHostViewAnimationType::Fade: + return std::make_tuple(YES, UIModalTransitionStyleCrossDissolve); + } +} + +static UIModalPresentationStyle presentationConfiguration(ModalHostViewProps const &props) +{ + if (props.transparent) { + return UIModalPresentationOverFullScreen; + } + switch (props.presentationStyle) { + case ModalHostViewPresentationStyle::FullScreen: + return UIModalPresentationFullScreen; + case ModalHostViewPresentationStyle::PageSheet: + return UIModalPresentationPageSheet; + case ModalHostViewPresentationStyle::FormSheet: + return UIModalPresentationFormSheet; + case ModalHostViewPresentationStyle::OverFullScreen: + return UIModalPresentationOverFullScreen; + } +} + +static ModalHostViewOnOrientationChangeStruct onOrientationChangeStruct(CGRect rect) +{ + auto orientation = rect.size.width < rect.size.height ? ModalHostViewOnOrientationChangeOrientationStruct::Portrait + : ModalHostViewOnOrientationChangeOrientationStruct::Landscape; + return {orientation}; +} + +@interface RNModalHostViewComponentView () + +@end + +@implementation RNModalHostViewComponentView { + RNModalHostViewController *_viewController; + ModalHostViewShadowNode::ConcreteState::Shared _state; + BOOL _shouldAnimatePresentation; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + _shouldAnimatePresentation = YES; + _viewController = [RNModalHostViewController new]; + _viewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical; + _viewController.delegate = self; + } + + return self; +} + +- (BOOL)isViewControllerPresented +{ + return _viewController.presentingViewController != nil; +} + +- (void)ensurePresentedOnlyIfNeeded +{ + BOOL shouldBePresented = !self.isViewControllerPresented && self.window; + if (shouldBePresented) { + UIViewController *controller = [self reactViewController]; + return [controller + presentViewController:_viewController + animated:_shouldAnimatePresentation + completion:^{ + ModalHostViewOnShowStruct onShow; + std::dynamic_pointer_cast(self->_eventEmitter)->onShow(onShow); + }]; + } + + BOOL shouldBeHidden = self.isViewControllerPresented && !self.superview; + if (shouldBeHidden) { + [_viewController dismissViewControllerAnimated:_shouldAnimatePresentation completion:nil]; + } +} + +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + [self ensurePresentedOnlyIfNeeded]; +} + +- (void)didMoveToSuperview +{ + [super didMoveToSuperview]; + [self ensurePresentedOnlyIfNeeded]; +} + +#pragma mark - RNModalHostViewControllerDelegate + +- (void)boundsDidChange:(CGRect)newBounds +{ + std::dynamic_pointer_cast(_eventEmitter) + ->onOrientationChange(onOrientationChangeStruct(newBounds)); + + if (_state != nullptr) { + auto newState = ModalHostViewState{RCTSizeFromCGSize(newBounds.size)}; + _state->updateState(std::move(newState)); + } +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + const auto &newProps = *std::static_pointer_cast(props); + +#if !TARGET_OS_TV + _viewController.supportedInterfaceOrientations = supportedOrientationsMask(newProps.supportedOrientations); +#endif + + std::tuple result = animationConfiguration(newProps.animationType); + _shouldAnimatePresentation = std::get<0>(result); + _viewController.modalTransitionStyle = std::get<1>(result); + + _viewController.modalPresentationStyle = presentationConfiguration(newProps); + + [super updateProps:props oldProps:oldProps]; +} + +- (void)updateState:(facebook::react::State::Shared const &)state + oldState:(facebook::react::State::Shared const &)oldState +{ + _state = std::static_pointer_cast(state); +} + +- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +{ + [_viewController.view insertSubview:childComponentView atIndex:index]; +} + +- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +{ + [childComponentView removeFromSuperview]; +} + +@end diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewController.h b/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewController.h new file mode 100644 index 00000000000..392597b7924 --- /dev/null +++ b/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewController.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@protocol RNModalHostViewControllerDelegate +- (void)boundsDidChange:(CGRect)newBounds; +@end + +@interface RNModalHostViewController : UIViewController + +@property (nonatomic, weak) id delegate; + +#if !TARGET_OS_TV +@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; +#endif + +@end diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewController.mm b/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewController.mm new file mode 100644 index 00000000000..ed91c89c1be --- /dev/null +++ b/React/Fabric/Mounting/ComponentViews/Modal/RNModalHostViewController.mm @@ -0,0 +1,94 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNModalHostViewController.h" + +#import +#import + +@implementation RNModalHostViewController { + CGRect _lastViewBounds; + RCTSurfaceTouchHandler *_touchHandler; +} + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + _touchHandler = [RCTSurfaceTouchHandler new]; + + return self; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + if (!CGRectEqualToRect(_lastViewBounds, self.view.bounds)) { + [_delegate boundsDidChange:self.view.bounds]; + _lastViewBounds = self.view.bounds; + } +} + +- (void)loadView +{ + [super loadView]; + [_touchHandler attachToView:self.view]; +} + +#if !TARGET_OS_TV +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return [RCTSharedApplication() statusBarStyle]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + _lastViewBounds = CGRectZero; +} + +- (BOOL)prefersStatusBarHidden +{ + return [RCTSharedApplication() isStatusBarHidden]; +} + +- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)())completion +{ + UIView *snapshot = [self.view snapshotViewAfterScreenUpdates:NO]; + [self.view addSubview:snapshot]; + + [super dismissViewControllerAnimated:flag + completion:^{ + [snapshot removeFromSuperview]; + if (completion) { + completion(); + } + }]; +} + +#if RCT_DEV +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + UIInterfaceOrientationMask appSupportedOrientationsMask = + [RCTSharedApplication() supportedInterfaceOrientationsForWindow:[RCTSharedApplication() keyWindow]]; + if (!(_supportedInterfaceOrientations & appSupportedOrientationsMask)) { + RCTLogError( + @"Modal was presented with 0x%x orientations mask but the application only supports 0x%x." + @"Add more interface orientations to your app's Info.plist to fix this." + @"NOTE: This will crash in non-dev mode.", + (unsigned)_supportedInterfaceOrientations, + (unsigned)appSupportedOrientationsMask); + return UIInterfaceOrientationMaskAll; + } + + return _supportedInterfaceOrientations; +} +#endif // RCT_DEV +#endif // !TARGET_OS_TV + +@end diff --git a/React/Fabric/Mounting/RCTComponentViewFactory.mm b/React/Fabric/Mounting/RCTComponentViewFactory.mm index 013324402cf..0e061824327 100644 --- a/React/Fabric/Mounting/RCTComponentViewFactory.mm +++ b/React/Fabric/Mounting/RCTComponentViewFactory.mm @@ -23,6 +23,7 @@ #import "RCTSwitchComponentView.h" #import "RCTUnimplementedNativeComponentView.h" #import "RCTViewComponentView.h" +#import "RNModalHostViewComponentView.h" #import "RNPullToRefreshViewComponentView.h" using namespace facebook::react; @@ -47,6 +48,7 @@ using namespace facebook::react; [componentViewFactory registerComponentViewClass:[RCTSliderComponentView class]]; [componentViewFactory registerComponentViewClass:[RCTSwitchComponentView class]]; [componentViewFactory registerComponentViewClass:[RCTUnimplementedNativeComponentView class]]; + [componentViewFactory registerComponentViewClass:[RNModalHostViewComponentView class]]; return componentViewFactory; } diff --git a/ReactCommon/fabric/components/modal/ModalHostViewComponentDescriptor.h b/ReactCommon/fabric/components/modal/ModalHostViewComponentDescriptor.h index a9a574a5582..7f8b7c691ad 100644 --- a/ReactCommon/fabric/components/modal/ModalHostViewComponentDescriptor.h +++ b/ReactCommon/fabric/components/modal/ModalHostViewComponentDescriptor.h @@ -21,8 +21,12 @@ namespace react { class ModalHostViewComponentDescriptor final : public ConcreteComponentDescriptor { public: +#ifdef ANDROID ModalHostViewComponentDescriptor(EventDispatcher::Shared eventDispatcher) : ConcreteComponentDescriptor(eventDispatcher) {} +#else + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; +#endif void adopt(UnsharedShadowNode shadowNode) const override { assert(std::dynamic_pointer_cast(shadowNode));