Files
react-navigation/ios/RNSScreen.m
Krzysztof Magiera 656e82de9f Dispatch appear event for screens. (#248)
Appear event is used by react-navigation to properly dispatch focus. It is important that appear is dispatched after dismissed event. The reverse order of actions would result in getting react-navigation stack in a weird state.

It is relatively streightforward to implement onAppear event on iOS where we hook into didAppear callback from UIViewController. It gets dispatched in the right moment, that is when the transition is fully over.

On Android however it is much more tricky. There is no standard way to be notified from the fragment level that fragment transition finished. One way that is frequently recommended is to override Fragment.onCreateAnimation. However, this only works when custom transitions are provided (e.g. if we set the transition to use fade animation). As we want the platform native transition to be run by default we had to look for other ways. The current approach relies on fragment container's callbacks startViewTransition and endViewTransition, with the latter being triggered once the animation is over. We also need to take into account that a good starting point for the transition is when we call commit on fragment transaction. We use these two methods to determine if the fragment is instantiated (onCreate) within a running transaction and if so we schedule event dispatch at the moment when endViewTransition is called.

Another change this commit introduces on the Android side is that we no longer rely on show/hide for replacing fragments on stack and we now use add/remove transaction methods. Due to this change we had to make our fragments reusable and make onCreateView indempotent.
2019-12-11 22:28:19 +01:00

308 lines
8.6 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 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;
}
- (void)setStackAnimation:(RNSScreenStackAnimation)stackAnimation
{
_stackAnimation = stackAnimation;
switch (stackAnimation) {
case RNSScreenStackAnimationFade:
_controller.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
break;
case RNSScreenStackAnimationFlip:
_controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
break;
case RNSScreenStackAnimationNone:
case RNSScreenStackAnimationDefault:
// Default
break;
}
}
- (UIView *)reactSuperview
{
return _reactSuperview;
}
- (void)addSubview:(UIView *)view
{
if (![view isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
[super addSubview:view];
} else {
((RNSScreenStackHeaderConfig*) view).screenView = self;
}
}
- (void)notifyFinishTransitioning
{
[_controller notifyFinishTransitioning];
}
- (void)notifyDismissed
{
if (self.onDismissed) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.onDismissed) {
self.onDismissed(nil);
}
});
}
}
- (void)notifyAppear
{
if (self.onAppear) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.onAppear) {
self.onAppear(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)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[((RNSScreenView *)self.view) notifyAppear];
}
- (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(onAppear, RCTDirectEventBlock);
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),
@"flip": @(RNSScreenStackAnimationFlip),
}), RNSScreenStackAnimationDefault, integerValue)
@end