Skip to content

Commit

Permalink
fix: iOS Fabric snapshotting mechanism (#1357)
Browse files Browse the repository at this point in the history
This PR moves the snapshotting logic from Screen to ScreenStack. From now on a snapshot is made from only the top-most screen on the stack.

Snapshots are used in order to have a transition animation during the go-back invoked from the JS side.

Problems:

 - fix snapshots in nested stacks that break the flow - done
 - only make a snapshot of the top-most screen - done
 - swipe to go back gesture shouldn't be invoked on the first screen in a stack - done
 - layout shift on initial render - TBD
 - lack of animation in the nested stack on initial app load - done

Co-authored-by: Wojciech Lewicki <[email protected]>
  • Loading branch information
kacperkapusciak and WoLewicki authored Mar 31, 2022
1 parent a12e257 commit 0bfc63f
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 65 deletions.
19 changes: 7 additions & 12 deletions ios/RNSScreenComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#import "RNSScreenStackHeaderConfigComponentView.h"

#import <React/RCTConversions.h>
#import <React/RCTMountingTransactionObserving.h>

#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
Expand All @@ -13,7 +12,7 @@

using namespace facebook::react;

@interface RNSScreenComponentView () <RCTRNSScreenViewProtocol, RCTMountingTransactionObserving>
@interface RNSScreenComponentView () <RCTRNSScreenViewProtocol>
@end

@implementation RNSScreenComponentView {
Expand Down Expand Up @@ -43,7 +42,6 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[self.controller setViewToSnapshot];
if ([childComponentView isKindOfClass:[RNSScreenStackHeaderConfigComponentView class]]) {
_config = nil;
}
Expand All @@ -53,8 +51,7 @@ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompo
- (void)updateBounds
{
if (_state != nullptr) {
auto boundsSize = self.bounds.size;
auto newState = RNSScreenState{RCTSizeFromCGSize(boundsSize)};
auto newState = RNSScreenState{RCTSizeFromCGSize(self.bounds.size)};
_state->updateState(std::move(newState));
UINavigationController *navctr = _controller.navigationController;
[navctr.view setNeedsLayout];
Expand All @@ -66,6 +63,11 @@ - (UIView *)reactSuperview
return _reactSuperview;
}

- (UIViewController *)reactViewController
{
return _controller;
}

- (void)notifyWillAppear
{
// If screen is already unmounted then there will be no event emitter
Expand Down Expand Up @@ -115,13 +117,6 @@ - (void)notifyDisappear
}
}

#pragma mark - RCTMountingTransactionObserving

- (void)mountingTransactionWillMountWithMetadata:(MountingTransactionMetadata const &)metadata
{
[self.controller takeSnapshot];
}

#pragma mark - RCTComponentViewProtocol

- (void)prepareForRecycle
Expand Down
3 changes: 1 addition & 2 deletions ios/RNSScreenController.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
@interface RNSScreenController : UIViewController

- (instancetype)initWithView:(UIView *)view;
- (void)takeSnapshot;
- (void)setViewToSnapshot;
- (void)setViewToSnapshot:(UIView *)snapshot;
- (void)resetViewToScreen;

@end
14 changes: 3 additions & 11 deletions ios/RNSScreenController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
#import "RNSScreenComponentView.h"

@implementation RNSScreenController {
CGRect _lastViewFrame;
RNSScreenComponentView *_initialView;
UIView *_snapshot;
}

- (instancetype)initWithView:(UIView *)view
Expand All @@ -20,15 +18,10 @@ - (instancetype)initWithView:(UIView *)view
return self;
}

- (void)takeSnapshot
{
_snapshot = [self.view snapshotViewAfterScreenUpdates:NO];
}

- (void)setViewToSnapshot
- (void)setViewToSnapshot:(UIView *)snapshot
{
[self.view removeFromSuperview];
self.view = _snapshot;
self.view = snapshot;
}

- (void)resetViewToScreen
Expand Down Expand Up @@ -70,8 +63,7 @@ - (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
BOOL isDisplayedWithinUINavController = [self.parentViewController isKindOfClass:[UINavigationController class]];
if (isDisplayedWithinUINavController && !CGRectEqualToRect(_lastViewFrame, self.view.frame)) {
_lastViewFrame = self.view.frame;
if (isDisplayedWithinUINavController) {
[_initialView updateBounds];
}
}
Expand Down
78 changes: 38 additions & 40 deletions ios/RNSScreenStackComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#import "RNSScreenComponentView.h"
#import "RNSScreenStackHeaderConfigComponentView.h"

#import <React/RCTMountingTransactionObserving.h>

#import <React/UIView+React.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
Expand All @@ -16,13 +18,15 @@ @interface RNSScreenStackComponentView () <
UINavigationControllerDelegate,
UIAdaptivePresentationControllerDelegate,
UIGestureRecognizerDelegate,
UIViewControllerTransitioningDelegate>
UIViewControllerTransitioningDelegate,
RCTMountingTransactionObserving>
@end

@implementation RNSScreenStackComponentView {
UINavigationController *_controller;
NSMutableArray<RNSScreenComponentView *> *_reactSubviews;
BOOL _invalidated;
UIView *_snapshot;
}

- (instancetype)initWithFrame:(CGRect)frame
Expand All @@ -34,22 +38,11 @@ - (instancetype)initWithFrame:(CGRect)frame
_controller = [[UINavigationController alloc] init];
_controller.delegate = self;
[_controller setViewControllers:@[ [UIViewController new] ]];
_invalidated = NO;
}

return self;
}

- (void)markChildUpdated
{
// do nothing
}

- (void)didUpdateChildren
{
// do nothing
}

- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if (![childComponentView isKindOfClass:[RNSScreenComponentView class]]) {
Expand All @@ -75,6 +68,11 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RNSScreenComponentView *screenChildComponent = (RNSScreenComponentView *)childComponentView;
// We should only do a snapshot of a screen that is on the top
if (screenChildComponent == _controller.topViewController.view) {
[screenChildComponent.controller setViewToSnapshot:_snapshot];
}

RCTAssert(
screenChildComponent.reactSuperview == self,
@"Attempt to unmount a view which is mounted inside different view. (parent: %@, child: %@, index: %@)",
Expand All @@ -97,6 +95,16 @@ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompo
});
}

- (void)takeSnapshot
{
_snapshot = [_controller.topViewController.view snapshotViewAfterScreenUpdates:NO];
}

- (void)mountingTransactionWillMountWithMetadata:(MountingTransactionMetadata const &)metadata
{
[self takeSnapshot];
}

- (NSArray<UIView *> *)reactSubviews
{
return _reactSubviews;
Expand All @@ -105,13 +113,8 @@ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompo
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (!_invalidated) {
// We check whether the view has been invalidated before running side-effects in didMoveToWindow
// This is needed because when LayoutAnimations are used it is possible for view to be re-attached
// to a window despite the fact it has been removed from the React Native view hierarchy.
// See https://github.com/software-mansion/react-native-screens/pull/700
[self maybeAddToParentAndUpdateContainer];
}
// for handling nested stacks
[self maybeAddToParentAndUpdateContainer];
}

- (void)navigationController:(UINavigationController *)navigationController
Expand Down Expand Up @@ -161,6 +164,7 @@ - (void)reactAddControllerToClosestParent:(UIViewController *)controller
_controller.interactivePopGestureRecognizer.delegate = self;
#endif
[self navigationController:_controller willShowViewController:_controller.topViewController animated:NO];
break;
}
parentView = (UIView *)parentView.reactSuperview;
}
Expand All @@ -182,21 +186,19 @@ - (void)setPushViewControllers:(NSArray<UIViewController *> *)controllers
}

UIViewController *top = controllers.lastObject;
UIViewController *lastTop = _controller.viewControllers.lastObject;
UIViewController *previousTop = _controller.topViewController;

// at the start we set viewControllers to contain a single UIVIewController
// At the start we set viewControllers to contain a single UIViewController
// instance. This is a workaround for header height adjustment bug (see comment
// in the init function). Here, we need to detect if the initial empty
// controller is still there
BOOL firstTimePush = ![lastTop isKindOfClass:[RNSScreenController class]];

BOOL shouldAnimate = YES;
BOOL firstTimePush = ![previousTop isKindOfClass:[RNSScreenController class]];

if (firstTimePush) {
// nothing pushed yet
[_controller setViewControllers:controllers animated:NO];
} else if (top != lastTop) {
if (![controllers containsObject:lastTop]) {
} else if (top != previousTop) {
if (![controllers containsObject:previousTop]) {
// if the previous top screen does not exist anymore and the new top was not on the stack before, probably replace
// was called, so we check the animation
if (![_controller.viewControllers containsObject:top]) {
Expand All @@ -209,9 +211,9 @@ - (void)setPushViewControllers:(NSArray<UIViewController *> *)controllers
// in this case we set the controllers stack to the new list with
// added the last top element to it and perform (animated) pop
NSMutableArray *newControllers = [NSMutableArray arrayWithArray:controllers];
[newControllers addObject:lastTop];
[newControllers addObject:previousTop];
[_controller setViewControllers:newControllers animated:NO];
[_controller popViewControllerAnimated:shouldAnimate];
[_controller popViewControllerAnimated:YES];
}
} else if (![_controller.viewControllers containsObject:top]) {
// new top controller is not on the stack
Expand All @@ -222,11 +224,11 @@ - (void)setPushViewControllers:(NSArray<UIViewController *> *)controllers
[_controller setViewControllers:newControllers animated:NO];
auto screenController = (RNSScreenController *)top;
[screenController resetViewToScreen];
[_controller pushViewController:top animated:shouldAnimate];
[_controller pushViewController:top animated:YES];
} else {
// don't really know what this case could be, but may need to handle it
// somehow
[_controller setViewControllers:controllers animated:shouldAnimate];
[_controller setViewControllers:controllers animated:YES];
}
} else {
// change wasn't on the top of the stack. We don't need animation.
Expand All @@ -239,12 +241,7 @@ - (void)updateContainer
NSMutableArray<UIViewController *> *pushControllers = [NSMutableArray new];
for (RNSScreenComponentView *screen in _reactSubviews) {
if (screen.controller != nil) {
if (pushControllers.count == 0) {
// first screen on the list needs to be places as "push controller"
[pushControllers addObject:screen.controller];
} else {
[pushControllers addObject:screen.controller];
}
[pushControllers addObject:screen.controller];
}
}

Expand All @@ -259,15 +256,16 @@ - (void)layoutSubviews

- (void)dismissOnReload
{
auto screenController = (RNSScreenController *)_controller.viewControllers.lastObject;
auto screenController = (RNSScreenController *)_controller.topViewController;
[screenController resetViewToScreen];
}

#pragma mark methods connected to transitioning
#pragma mark - methods connected to transitioning

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
return YES;
// you shouldn't be able to use gesture to go back when there is just one screen
return _controller.viewControllers.count >= 2;
}

#pragma mark - RCTComponentViewProtocol
Expand All @@ -277,9 +275,9 @@ - (void)prepareForRecycle
[super prepareForRecycle];
_reactSubviews = [NSMutableArray new];
[self dismissOnReload];
_invalidated = YES;
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
[_controller setViewControllers:@[ [UIViewController new] ]];
}

+ (ComponentDescriptorProvider)componentDescriptorProvider
Expand Down

0 comments on commit 0bfc63f

Please sign in to comment.