Files
react-navigation/ios/RNSScreen.m
Janic Duplessis 535902014d Fix non-default modal presentation styles (#185)
* Fix non-default modal presentation styles

Using alternative modal presentation styles like `transparentModal` is not working. This is because accessing `presentationController` before setting the `modalPresentationStyle` causes the presentation controller to be created and cannot be changed afterwards. This is documented https://developer.apple.com/documentation/uikit/uiviewcontroller/1621426-presentationcontroller?language=objc.

Yes very cool API

Tested via @react-navigation/native-stack using presentation: 'transparentModal'.

* Update RNSScreen.m
2019-10-21 09:01:02 +02:00

302 lines
8.4 KiB
Objective-C

#import <UIKit/UIKit.h>
#import "RNSScreen.h"
#import "RNSScreenContainer.h"
#import "RNSScreenStackHeaderConfig.h"
#import <React/RCTUIManager.h>
#import <React/RCTShadowView.h>
#import <React/RCTTouchHandler.h>
@interface RNSScreenFrameData : NSObject
@property (nonatomic, readonly) CGFloat rightInset;
@property (nonatomic, readonly) CGFloat topInset;
@property (nonatomic, readonly) CGFloat bottomInset;
@property (nonatomic, readonly) CGFloat leftInset;
@property (nonatomic, readonly) CGFloat navbarOffset;
- (instancetype)initWithInsets:(UIEdgeInsets)insets;
@end
@implementation RNSScreenFrameData
- (instancetype)initWithInsets:(UIEdgeInsets)insets andNavbarOffset:(CGFloat)navbarOffset
{
if (self = [super init]) {
_topInset = insets.top;
_bottomInset = insets.bottom;
_leftInset = insets.left;
_rightInset = insets.right;
_navbarOffset = navbarOffset;
}
return self;
}
@end
@interface RNSScreen : UIViewController
- (instancetype)initWithView:(UIView *)view;
- (void)notifyFinishTransitioning;
@end
@interface RNSScreenView () <UIAdaptivePresentationControllerDelegate>
@end
@implementation RNSScreenView {
__weak RCTBridge *_bridge;
RNSScreen *_controller;
RCTTouchHandler *_touchHandler;
}
@synthesize controller = _controller;
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
_controller = [[RNSScreen alloc] initWithView:self];
_stackPresentation = RNSScreenStackPresentationPush;
_stackAnimation = RNSScreenStackAnimationDefault;
}
return self;
}
- (void)reactSetFrame:(CGRect)frame
{
// ignore setFrame call from react, the frame of this view
// is controlled by the UIViewController it is contained in
}
- (void)updateBounds
{
[_bridge.uiManager setSize:self.bounds.size forView:self];
}
- (void)setActive:(BOOL)active
{
if (active != _active) {
_active = active;
[_reactSuperview markChildUpdated];
}
}
- (void)setPointerEvents:(RCTPointerEvents)pointerEvents
{
// pointer events settings are managed by the parent screen container, we ignore
// any attempt of setting that via React props
}
- (void)setStackPresentation:(RNSScreenStackPresentation)stackPresentation
{
_stackPresentation = stackPresentation;
switch (stackPresentation) {
case RNSScreenStackPresentationModal:
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
_controller.modalPresentationStyle = UIModalPresentationAutomatic;
} else {
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
}
#else
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
#endif
break;
case RNSScreenStackPresentationTransparentModal:
_controller.modalPresentationStyle = UIModalPresentationOverFullScreen;
break;
case RNSScreenStackPresentationContainedModal:
_controller.modalPresentationStyle = UIModalPresentationCurrentContext;
break;
case RNSScreenStackPresentationContainedTransparentModal:
_controller.modalPresentationStyle = UIModalPresentationOverCurrentContext;
break;
}
// `modalPresentationStyle` must be set before accessing `presentationController`
// otherwise a default controller will be created and cannot be changed after.
// Documented here: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621426-presentationcontroller?language=objc
_controller.presentationController.delegate = self;
}
- (UIView *)reactSuperview
{
return _reactSuperview;
}
- (void)addSubview:(UIView *)view
{
if (![view isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
[super addSubview:view];
}
}
- (void)notifyFinishTransitioning
{
[_controller notifyFinishTransitioning];
}
- (void)notifyDismissed
{
if (self.onDismissed) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.onDismissed) {
self.onDismissed(nil);
}
});
}
}
- (BOOL)isMountedUnderScreenOrReactRoot
{
for (UIView *parent = self.superview; parent != nil; parent = parent.superview) {
if ([parent isKindOfClass:[RCTRootView class]] || [parent isKindOfClass:[RNSScreenView class]]) {
return YES;
}
}
return NO;
}
- (void)didMoveToWindow
{
// For RN touches to work we need to instantiate and connect RCTTouchHandler. This only applies
// for screens that aren't mounted under RCTRootView e.g., modals that are mounted directly to
// root application window.
if (self.window != nil && ![self isMountedUnderScreenOrReactRoot]) {
if (_touchHandler == nil) {
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
}
[_touchHandler attachToView:self];
} else {
[_touchHandler detachFromView:self];
}
}
- (void)presentationControllerWillDismiss:(UIPresentationController *)presentationController
{
// We need to call both "cancel" and "reset" here because RN's gesture recognizer
// does not handle the scenario when it gets cancelled by other top
// level gesture recognizer. In this case by the modal dismiss gesture.
// Because of that, at the moment when this method gets called the React's
// gesture recognizer is already in FAILED state but cancel events never gets
// send to JS. Calling "reset" forces RCTTouchHanler to dispatch cancel event.
// To test this behavior one need to open a dismissable modal and start
// pulling down starting at some touchable item. Without "reset" the touchable
// will never go back from highlighted state even when the modal start sliding
// down.
[_touchHandler cancel];
[_touchHandler reset];
}
@end
@implementation RNSScreen {
__weak UIView *_view;
__weak id _previousFirstResponder;
CGRect _lastViewFrame;
}
- (instancetype)initWithView:(UIView *)view
{
if (self = [super init]) {
_view = view;
}
return self;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!CGRectEqualToRect(_lastViewFrame, self.view.frame)) {
_lastViewFrame = self.view.frame;
[((RNSScreenView *)self.view) updateBounds];
}
}
- (id)findFirstResponder:(UIView*)parent
{
if (parent.isFirstResponder) {
return parent;
}
for (UIView *subView in parent.subviews) {
id responder = [self findFirstResponder:subView];
if (responder != nil) {
return responder;
}
}
return nil;
}
- (void)willMoveToParentViewController:(UIViewController *)parent
{
if (parent == nil) {
id responder = [self findFirstResponder:self.view];
if (responder != nil) {
_previousFirstResponder = responder;
}
}
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
if (self.parentViewController == nil && self.presentingViewController == nil) {
// screen dismissed, send event
[((RNSScreenView *)self.view) notifyDismissed];
}
}
- (void)notifyFinishTransitioning
{
[_previousFirstResponder becomeFirstResponder];
_previousFirstResponder = nil;
}
- (void)loadView
{
if (_view != nil) {
self.view = _view;
_view = nil;
}
}
@end
@implementation RNSScreenManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
RCT_EXPORT_VIEW_PROPERTY(stackPresentation, RNSScreenStackPresentation)
RCT_EXPORT_VIEW_PROPERTY(stackAnimation, RNSScreenStackAnimation)
RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTDirectEventBlock);
- (UIView *)view
{
return [[RNSScreenView alloc] initWithBridge:self.bridge];
}
@end
@implementation RCTConvert (RNSScreen)
RCT_ENUM_CONVERTER(RNSScreenStackPresentation, (@{
@"push": @(RNSScreenStackPresentationPush),
@"modal": @(RNSScreenStackPresentationModal),
@"containedModal": @(RNSScreenStackPresentationContainedModal),
@"transparentModal": @(RNSScreenStackPresentationTransparentModal),
@"containedTransparentModal": @(RNSScreenStackPresentationContainedTransparentModal)
}), RNSScreenStackPresentationPush, integerValue)
RCT_ENUM_CONVERTER(RNSScreenStackAnimation, (@{
@"default": @(RNSScreenStackAnimationDefault),
@"none": @(RNSScreenStackAnimationNone),
@"fade": @(RNSScreenStackAnimationFade)
}), RNSScreenStackAnimationDefault, integerValue)
@end