Files
react-navigation/ios/RNSScreenStack.m
2018-08-03 16:20:01 +02:00

244 lines
7.5 KiB
Objective-C

#import "RNSScreenStack.h"
#import "RNSScreen.h"
#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerUtils.h>
@interface RNSCustomAnimator : NSObject <UIViewControllerAnimatedTransitioning>
@property (nonatomic) BOOL presenting;
@end
@implementation RNSCustomAnimator
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *container = transitionContext.containerView;
if (_presenting) {
[container addSubview:toView];
toView.alpha = 0.99;
} else {
[container insertSubview:toView belowSubview:fromView];
}
// When view is added to UINavController it flattens view's translation into frame and makes it
// so that the view with translation applied is centered. We often don't want that as we want the
// view to animated from side. In order to achieve this we reset the translation applied to the
// view such that the frame is at point 0,0 and translation adds an offset from that position.
CATransform3D origTransform = toView.layer.transform;
toView.transform = CGAffineTransformIdentity;
toView.frame = CGRectMake(0, 0, toView.frame.size.width, toView.frame.size.height);
toView.layer.transform = origTransform;
[UIView
animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
// We need to animate at least one property, otherwise the interactive animation wouldn't
// behave as expected. I haven't had enough time to investigate and hence as a workaround
// we are animating alpha from 1 to 0.99
if (_presenting) {
toView.alpha = 1.0;
} else {
fromView.alpha = 0.99;
}
} completion:^(BOOL finished) {
BOOL success = !transitionContext.transitionWasCancelled;
if (!success) {
[toView removeFromSuperview];
}
[transitionContext completeTransition:success];
}];
}
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
// as long as it is non-zero it does not matter what value is here, we turn animation into
// "interactive" mode anyways which make it be controlled by "progress" property anyways.
return 0.1;
}
@end
@interface RNSScreenStackView () <UINavigationControllerDelegate>
@property (nonatomic) NSInteger transitioning;
@property (nonatomic) CGFloat progress;
@end
@implementation RNSScreenStackView {
BOOL _needUpdate;
BOOL _transitioningStateChanged;
UINavigationController *_controller;
NSMutableSet<RNSScreenView *> *_activeScreens;
NSMutableArray<RNSScreenView *> *_reactSubviews;
UIPercentDrivenInteractiveTransition *_interactor;
__weak RNSScreenStackManager *_manager;
}
- (instancetype)initWithManager:(RNSScreenStackManager*)manager
{
if (self = [super init]) {
_manager = manager;
_reactSubviews = [NSMutableArray new];
_controller = [[UINavigationController alloc] init];
_controller.navigationBarHidden = YES;
_controller.delegate = self;
_needUpdate = NO;
[self addSubview:_controller.view];
}
return self;
}
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
return _interactor;
}
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
RNSCustomAnimator *animator = [RNSCustomAnimator new];
animator.presenting = _transitioning > 0;
return animator;
}
- (void)markUpdated
{
// We want 'updateContainer' to be executed on main thread after all enqueued operations in
// uimanager are complete. In order to achieve that we enqueue call on UIManagerQueue from which
// we enqueue call on the main queue. This seems to be working ok in all the cases I've tried but
// there is a chance it is not the correct way to do that.
if (!_needUpdate) {
_needUpdate = YES;
RCTExecuteOnUIManagerQueue(^{
RCTExecuteOnMainQueue(^{
_needUpdate = NO;
[self updateContainer];
});
});
}
}
- (void)markChildUpdated
{
// do nothing
}
- (void)didUpdateChildren
{
// do nothing
}
- (void)setProgress:(CGFloat)progress
{
_progress = progress;
[_interactor updateInteractiveTransition:progress];
}
- (void)setTransitioning:(NSInteger)transitioning
{
if (_transitioning == transitioning) {
return;
}
_transitioningStateChanged = YES;
_transitioning = transitioning;
}
- (void)insertReactSubview:(RNSScreenView *)subview atIndex:(NSInteger)atIndex
{
subview.hidden = NO;
subview.reactSuperview = self;
[_reactSubviews insertObject:subview atIndex:atIndex];
[self markUpdated];
}
- (void)removeReactSubview:(RNSScreenView *)subview
{
// Right after the view gets removed properties such as transform will get reset. In addition to
// that UINavigationController takes a snapshot of the view before it gets completely unmounted.
// This causes an effect in which even so the view are detached from the hierarchy they could still
// be visible for a frame. This is often undesirable e.g. in a case when we want to slide view
// outside of the visible bounds, because as a result it will jump back to position 0,0 right before
// the transition is over. To prevent that we hide the view right before removing it from the subviews
// array.
subview.hidden = YES;
subview.reactSuperview = nil;
[_reactSubviews removeObject:subview];
[self markUpdated];
}
- (NSArray<UIView *> *)reactSubviews
{
return _reactSubviews;
}
- (void)didUpdateReactSubviews
{
// do nothing
}
- (void)updateContainer
{
NSMutableArray<UIViewController *> *controllers = [NSMutableArray new];
for (RNSScreenView *screen in _reactSubviews) {
[controllers addObject:screen.controller];
}
if (_transitioningStateChanged) {
if (_transitioning == 0) {
// finish or cancel transitioning
if (_controller.viewControllers.lastObject == controllers.lastObject) {
[_interactor finishInteractiveTransition];
} else {
[_interactor cancelInteractiveTransition];
}
} else {
_interactor = [UIPercentDrivenInteractiveTransition new];
if (_transitioning < 0) {
[_controller setViewControllers:controllers animated:NO];
[_controller popViewControllerAnimated:YES];
} else {
UIViewController *lastController = [controllers lastObject];
[controllers removeLastObject];
[_controller setViewControllers:controllers animated:NO];
[_controller pushViewController:lastController animated:YES];
}
}
_transitioningStateChanged = NO;
} else {
[_controller setViewControllers:controllers animated:NO];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self reactAddControllerToClosestParent:_controller];
_controller.view.frame = self.bounds;
}
@end
@implementation RNSScreenStackManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(transitioning, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(progress, CGFloat)
- (UIView *)view
{
return [[RNSScreenStackView alloc] initWithManager:self];
}
@end