Files
react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm
T
Samuel Susla 33c16689a8 Deallocate UIViewController after it was used in Modal component
Summary:
Changelog: [Internal]

# Problem
`RCTModalHostViewComponentView` reuses the same UIViewController between recycles. This can be a problem if dismissal of the modal is animated as the UIViewController is still presented while it is being dismissed. That's when "Application tried to present modally an active controller" exception can happen.

# Solution
Deallocate UIViewController when `RCTModalHostViewComponentView` is recycled.

Reviewed By: PeteTheHeat

Differential Revision: D23345711

fbshipit-source-id: da540571184afcb88b52758c4a1f0b4ec5874eb1
2020-08-26 18:49:21 -07:00

245 lines
7.6 KiB
Plaintext

/*
* 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 "RCTModalHostViewComponentView.h"
#import <React/UIView+React.h>
#import <react/renderer/components/modal/ModalHostViewComponentDescriptor.h>
#import <react/renderer/components/modal/ModalHostViewState.h>
#import <react/renderer/components/rncore/EventEmitters.h>
#import <react/renderer/components/rncore/Props.h>
#import "RCTConversions.h"
#import "RCTFabricModalHostViewController.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<BOOL, UIModalTransitionStyle> 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 ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(CGRect rect)
{
;
auto orientation = rect.size.width < rect.size.height
? ModalHostViewEventEmitter::OnOrientationChangeOrientation::Portrait
: ModalHostViewEventEmitter::OnOrientationChangeOrientation::Landscape;
return {orientation};
}
@interface RCTModalHostViewComponentView () <RCTFabricModalHostViewControllerDelegate>
@end
@implementation RCTModalHostViewComponentView {
RCTFabricModalHostViewController *_viewController;
ModalHostViewShadowNode::ConcreteStateTeller _stateTeller;
BOOL _shouldAnimatePresentation;
BOOL _isPresented;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const ModalHostViewProps>();
_props = defaultProps;
_shouldAnimatePresentation = YES;
_isPresented = NO;
}
return self;
}
- (RCTFabricModalHostViewController *)viewController
{
if (!_viewController) {
_viewController = [RCTFabricModalHostViewController new];
_viewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
_viewController.delegate = self;
}
return _viewController;
}
- (void)presentViewController:(UIViewController *)modalViewController
animated:(BOOL)animated
completion:(void (^)(void))completion
{
UIViewController *controller = [self reactViewController];
[controller presentViewController:modalViewController animated:animated completion:completion];
}
- (void)dismissViewController:(UIViewController *)modalViewController animated:(BOOL)animated
{
[modalViewController dismissViewControllerAnimated:animated completion:nil];
}
- (void)ensurePresentedOnlyIfNeeded
{
BOOL shouldBePresented = !_isPresented && self.window;
if (shouldBePresented) {
_isPresented = YES;
[self presentViewController:self.viewController
animated:_shouldAnimatePresentation
completion:^{
if (!self->_eventEmitter) {
return;
}
assert(std::dynamic_pointer_cast<ModalHostViewEventEmitter const>(self->_eventEmitter));
auto eventEmitter =
std::static_pointer_cast<ModalHostViewEventEmitter const>(self->_eventEmitter);
eventEmitter->onShow(ModalHostViewEventEmitter::OnShow{});
}];
}
BOOL shouldBeHidden = _isPresented && !self.superview;
if (shouldBeHidden) {
_isPresented = NO;
[self dismissViewController:self.viewController animated:_shouldAnimatePresentation];
}
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
[self ensurePresentedOnlyIfNeeded];
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
[self ensurePresentedOnlyIfNeeded];
}
#pragma mark - RCTFabricModalHostViewControllerDelegate
- (void)boundsDidChange:(CGRect)newBounds
{
if (_eventEmitter) {
assert(std::dynamic_pointer_cast<ModalHostViewEventEmitter const>(_eventEmitter));
auto eventEmitter = std::static_pointer_cast<ModalHostViewEventEmitter const>(_eventEmitter);
eventEmitter->onOrientationChange(onOrientationChangeStruct(newBounds));
}
auto newState = ModalHostViewState{RCTSizeFromCGSize(newBounds.size)};
_stateTeller.updateState(std::move(newState));
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<ModalHostViewComponentDescriptor>();
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_stateTeller.invalidate();
_viewController = nil;
_isPresented = NO;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &newProps = *std::static_pointer_cast<const ModalHostViewProps>(props);
#if !TARGET_OS_TV
self.viewController.supportedInterfaceOrientations = supportedOrientationsMask(newProps.supportedOrientations);
#endif
std::tuple<BOOL, UIModalTransitionStyle> result = animationConfiguration(newProps.animationType);
_shouldAnimatePresentation = std::get<0>(result);
self.viewController.modalTransitionStyle = std::get<1>(result);
self.viewController.modalPresentationStyle = presentationConfiguration(newProps);
[super updateProps:props oldProps:oldProps];
}
- (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState
{
_stateTeller.setConcreteState(state);
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[self.viewController.view insertSubview:childComponentView atIndex:index];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[childComponentView removeFromSuperview];
}
@end