Compare commits

..

16 Commits

Author SHA1 Message Date
Krzysztof Magiera
6f8b460549 Bump version -> 2.0.0-beta.13 2020-02-24 23:31:16 +01:00
Krzysztof Magiera
b52aea3fd2 Hotfix for the case when views are invalidated but not removed from subviews
The above situation may happen when grandparent is unmounted from react. In such a case children are not removed from non direct ancestors but still get invalidated.
2020-02-24 23:30:02 +01:00
Krzysztof Magiera
4e72ad050b Bump version -> 2.0.0-beta.12 2020-02-24 22:26:32 +01:00
Krzysztof Magiera
0dd03e2c53 Revert container frame update change from #354 (#379)
Reverting that change as it turned out not to be necessary. At the moment of attaching screen we already have the frame set on the child VC. Updating it there again was cauinsg some visual glitches with old react-navigation screen container based implementation.
2020-02-24 22:26:01 +01:00
Krzysztof Magiera
e38c5bfdc2 Fix memory leak related to access of presentationController. (#378)
After exaiming memory graph it turned out that screens for which we access presentationController property ends up leaking, likely because of some issue in UIKit. This turns out not to affect screens that are actually being presented as modals (when presentationController is required). This change updates presentation mode setter to avoid accessing presentationController for push screens and also warns if the prop ever switches from modal to push (in which case we cannot reverse the leak source).
2020-02-24 22:14:34 +01:00
Krzysztof Magiera
cdbf4e463f Bump version -> 2.0.0-beta.11 2020-02-24 18:06:09 +01:00
Krzysztof Magiera
4e8d13dc72 Verify integrity of fragment manager before executePendingTransa… (#377) 2020-02-24 18:03:46 +01:00
Krzysztof Magiera
d3b6bea594 Fix no view found crash in fragment manager. (#376)
This change fixes issue reported in #54. The issue was caused by the fragment transaction that run past the moment when container view is detached. This could happen when container is quickly added and removed as fragment transactions sometimes may take long time to execute. Unfortunately enqueued fragment transactions cannot be cancelled, so to make sure that transaction isn't run past container unmount we call executePendingTransactions right before the screen is detached.
2020-02-24 17:42:44 +01:00
Krzysztof Magiera
b29e634e26 Fix appear/disappear events for ScreenContainer and address memo… (#354)
This chnage fixes the way we'd managed parent<>child VC relation. With this change in we hook child VC to parent in didMoveToWindow to match Stack behavior. We also wait with updating child view frame untill the child screen is attached. Finally we utilize RCTInvalidating interface to spot moments when screen controller is unmounted from react such that we can break reference cycle between screen and screen view (we don't do it in invalidate directly as sometimes we need to wait till transition end).
2020-02-21 22:40:44 +01:00
Krzysztof Magiera
f2caf02d8c Fix didMoveToWindow logic. (#352)
This change fixes logic that we run when stack is moved to a window. Before we'd first attach stack to parent VC and then run updateContainer. This could've led to a buggy behavior in case the stack is mounted within other stack that is running a transition (e.g. dismissing a modal). In such a case setting initial push VCs would fail (because UIKit does not allow push to be modified while transitioning). Because of that stack would initialize with the dummy VC and the initial VC would get added to dismissedScreens (which is another side effect of the logic that keeps track of dismissed screens).

The fix was to add call to updateContainer before VC is added to parent. This makes it possible for push screens to be properly initialized (since VC is not yet added it does not know its parent is transitioning). Then, since updating modals handles gracefully the case when modals cannot be shown (and they wont be shown unless added to parent VC) we can safely run updateContainer yet another time after the VC is added to parent.

Howeer after the above fix was applied we observed another issue that was due to an invalida appear/disappear event management within view controllers. To address that fix we no longer allow navigaton VC's view to be added to the container view unless it starts transition to parent view controller. We also fixed missing calls to super in willMoveToParentViewController callback of RNSScreen controller and also fixed view reference management withing RNScreen to keep views as weak references while the view is not attached. The latter fixes the problem with screen view leaking under certain conditions.
2020-02-21 18:06:38 +01:00
Krzysztof Magiera
c8845cbb6a Bump version -> 2.0.0-beta.10 2020-02-21 00:17:28 +01:00
Krzysztof Magiera
9bf2edd405 Revert updateContainer threading changes from #351. 2020-02-21 00:16:43 +01:00
Krzysztof Magiera
748cdc6fba Bump version -> 2.0.0-beta.9 2020-02-20 18:05:28 +01:00
Krzysztof Magiera
89cf0b7e52 Fix detaching VC from parent VC. (#351)
This change fixes the problem of container view controllers not being detached properly from its parent container VC. We fix this by detaching VC from didMoveToWindow. On top of that we also added a check to ensure the update of containers won't run unless JS batch is complete. This is to prevent partially updated headers.
2020-02-20 17:50:20 +01:00
Janic Duplessis
db4733ad05 Fix gestureEnabled for modal with no header on iOS (#325)
Since the config is only updated in the HeaderConfig view it will not get executed for stacks with no header. This fixes it by setting the prop directly in the Screen when the gestureEnabled prop is set.
2020-02-19 20:33:58 +01:00
Radek Czemerys
8952e698d2 docs: Replace useScreens with enableScreens for expo docs (#348)
fixes #346
2020-02-19 18:09:25 +01:00
7 changed files with 113 additions and 38 deletions

View File

@@ -67,9 +67,9 @@ yarn add react-native-screens
2. Open your App.js file and add the following snippet somewhere near the top of the file (e.g. right after import statements):
```js
import { useScreens } from 'react-native-screens';
import { enableScreens } from 'react-native-screens';
useScreens();
enableScreens();
```
3. That's all 🎉  enjoy faster navigation in your Expo app. Keep in mind screens are in pretty early phase so please report if you discover some issues.

View File

@@ -218,6 +218,12 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
@Override
protected void onDetachedFromWindow() {
// if there are pending transactions and this view is about to get detached we need to perform
// them here as otherwise fragment manager will crash because it won't be able to find container
// view.
if (mFragmentManager != null && !mFragmentManager.isDestroyed()) {
mFragmentManager.executePendingTransactions();
}
super.onDetachedFromWindow();
mIsAttached = false;
}

View File

@@ -8,7 +8,7 @@
#import <React/RCTShadowView.h>
#import <React/RCTTouchHandler.h>
@interface RNSScreenView () <UIAdaptivePresentationControllerDelegate>
@interface RNSScreenView () <UIAdaptivePresentationControllerDelegate, RCTInvalidating>
@end
@implementation RNSScreenView {
@@ -46,6 +46,11 @@
// subviews
}
- (UIViewController *)reactViewController
{
return _controller;
}
- (void)updateBounds
{
[_bridge.uiManager setSize:self.bounds.size forView:self];
@@ -67,7 +72,6 @@
- (void)setStackPresentation:(RNSScreenStackPresentation)stackPresentation
{
_stackPresentation = stackPresentation;
switch (stackPresentation) {
case RNSScreenStackPresentationModal:
#ifdef __IPHONE_13_0
@@ -95,11 +99,23 @@
case RNSScreenStackPresentationContainedTransparentModal:
_controller.modalPresentationStyle = UIModalPresentationOverCurrentContext;
break;
case RNSScreenStackPresentationPush:
// ignored, we only need to keep in mind not to set presentation delegate
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;
// There is a bug in UIKit which causes retain loop when presentationController is accessed for a
// controller that is not going to be presented modally. We therefore need to avoid setting the
// delegate for screens presented using push. This also means that when controller is updated from
// modal to push type, this may cause memory leak, we warn about that as well.
if (stackPresentation != RNSScreenStackPresentationPush) {
// `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;
} else if (_stackPresentation != RNSScreenStackPresentationPush) {
RCTLogError(@"Screen presentation updated from modal to push, this may likely result in a screen object leakage. If you need to change presentation style create a new screen object instead");
}
_stackPresentation = stackPresentation;
}
- (void)setStackAnimation:(RNSScreenStackAnimation)stackAnimation
@@ -120,6 +136,17 @@
}
}
- (void)setGestureEnabled:(BOOL)gestureEnabled
{
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
_controller.modalInPresentation = !gestureEnabled;
}
#endif
_gestureEnabled = gestureEnabled;
}
- (UIView *)reactSuperview
{
return _reactSuperview;
@@ -215,10 +242,14 @@
}
}
- (void)invalidate
{
_controller = nil;
}
@end
@implementation RNSScreen {
__weak UIView *_view;
__weak id _previousFirstResponder;
CGRect _lastViewFrame;
}
@@ -226,7 +257,7 @@
- (instancetype)initWithView:(UIView *)view
{
if (self = [super init]) {
_view = view;
self.view = view;
}
return self;
}
@@ -237,7 +268,7 @@
if (!CGRectEqualToRect(_lastViewFrame, self.view.frame)) {
_lastViewFrame = self.view.frame;
[((RNSScreenView *)self.view) updateBounds];
[((RNSScreenView *)self.viewIfLoaded) updateBounds];
}
}
@@ -257,6 +288,7 @@
- (void)willMoveToParentViewController:(UIViewController *)parent
{
[super willMoveToParentViewController:parent];
if (parent == nil) {
id responder = [self findFirstResponder:self.view];
if (responder != nil) {
@@ -271,8 +303,6 @@
if (self.parentViewController == nil && self.presentingViewController == nil) {
// screen dismissed, send event
[((RNSScreenView *)self.view) notifyDismissed];
_view = self.view;
self.view = nil;
}
}
@@ -288,14 +318,6 @@
_previousFirstResponder = nil;
}
- (void)loadView
{
if (_view != nil) {
self.view = _view;
_view = nil;
}
}
@end
@implementation RNSScreenManager

View File

@@ -11,7 +11,7 @@
@end
@interface RNSScreenContainerView ()
@interface RNSScreenContainerView () <RCTInvalidating>
@property (nonatomic, retain) UIViewController *controller;
@property (nonatomic, retain) NSMutableSet<RNSScreenView *> *activeScreens;
@@ -68,6 +68,11 @@
return _reactSubviews;
}
- (UIViewController *)reactViewController
{
return _controller;
}
- (void)detachScreen:(RNSScreenView *)screen
{
[screen.controller willMoveToParentViewController:nil];
@@ -79,6 +84,9 @@
- (void)attachScreen:(RNSScreenView *)screen
{
[_controller addChildViewController:screen.controller];
// the frame is already set at this moment because we adjust it in insertReactSubview. We don't
// want to update it here as react-driven animations may already run and hence changing the frame
// would result in visual glitches
[_controller.view addSubview:screen.controller.view];
[screen.controller didMoveToParentViewController:_controller];
[_activeScreens addObject:screen];
@@ -155,10 +163,22 @@
[self markChildUpdated];
}
- (void)didMoveToWindow
{
if (self.window) {
[self reactAddControllerToClosestParent:_controller];
}
}
- (void)invalidate
{
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self reactAddControllerToClosestParent:_controller];
_controller.view.frame = self.bounds;
for (RNSScreenView *subview in _reactSubviews) {
subview.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);

View File

@@ -37,7 +37,6 @@
_dismissedScreens = [NSMutableSet new];
_controller = [[UINavigationController alloc] init];
_controller.delegate = self;
[self addSubview:_controller.view];
_controller.interactivePopGestureRecognizer.delegate = self;
// we have to initialize viewControllers with a non empty array for
@@ -49,6 +48,11 @@
return self;
}
- (UIViewController *)reactViewController
{
return _controller;
}
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
UIView *view = viewController.view;
@@ -174,10 +178,35 @@
{
[super didMoveToWindow];
if (self.window) {
// when stack is added to a window we try to update push and modal view controllers. It is
// because modal operations are blocked by UIKit when parent VC is not mounted, so we need
// to redo them when the stack is attached.
// when stack is attached to a window we do two things:
// 1) we run updateContainer we do this because we want push view controllers to be installed
// before the VC is mounted. If we do that after it is added to parent the push updates operations
// are going to be blocked by UIKit.
// 2) we add navigation VS to parent this is needed for the VC lifecycle events to be dispatched
// properly
// 3) we again call updateContainer this time we do this to open modal controllers. Modals
// won't open in (1) because they require navigator to be added to parent. We handle that case
// gracefully in setModalViewControllers and can retry opening at any point.
[self updateContainer];
[self reactAddControllerToClosestParent:_controller];
[self updateContainer];
}
}
- (void)reactAddControllerToClosestParent:(UIViewController *)controller
{
if (!controller.parentViewController) {
UIView *parentView = (UIView *)self.reactSuperview;
while (parentView) {
if (parentView.reactViewController) {
[parentView.reactViewController addChildViewController:controller];
[self addSubview:controller.view];
[controller didMoveToParentViewController:parentView.reactViewController];
break;
}
parentView = (UIView *)parentView.reactSuperview;
}
return;
}
}
@@ -243,7 +272,8 @@
[weakSelf.presentedModals
removeObjectsInRange:NSMakeRange(changeRootIndex, oldCount - changeRootIndex)];
}
if (changeRootController.view.window == nil || changeRootIndex >= controllers.count) {
BOOL isAttached = changeRootController.parentViewController != nil || changeRootController.presentingViewController != nil;
if (!isAttached || changeRootIndex >= controllers.count) {
// if change controller view is not attached, presenting modals will silently fail on iOS.
// In such a case we trigger controllers update from didMoveToWindow.
// We also don't run any present transitions if changeRootIndex is greater or equal to the size
@@ -299,7 +329,7 @@
// controller is still there
BOOL firstTimePush = ![lastTop isKindOfClass:[RNSScreen class]];
BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone && !_controller.presentedViewController;
BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone;
if (firstTimePush) {
// nothing pushed yet
@@ -337,7 +367,7 @@
NSMutableArray<UIViewController *> *pushControllers = [NSMutableArray new];
NSMutableArray<UIViewController *> *modalControllers = [NSMutableArray new];
for (RNSScreenView *screen in _reactSubviews) {
if (![_dismissedScreens containsObject:screen]) {
if (![_dismissedScreens containsObject:screen] && screen.controller != nil) {
if (pushControllers.count == 0) {
// first screen on the list needs to be places as "push controller"
[pushControllers addObject:screen.controller];
@@ -358,7 +388,6 @@
- (void)layoutSubviews
{
[super layoutSubviews];
[self reactAddControllerToClosestParent:_controller];
_controller.view.frame = self.bounds;
}
@@ -368,6 +397,8 @@
[controller dismissViewControllerAnimated:NO completion:nil];
}
[_presentedModals removeAllObjects];
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
}
- (void)dismissOnReload

View File

@@ -275,11 +275,7 @@
}
[navctr setNavigationBarHidden:shouldHide animated:YES];
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
vc.modalInPresentation = !config.screenView.gestureEnabled;
}
#endif
if (shouldHide) {
return;
}

View File

@@ -1,7 +1,7 @@
{
"name": "react-native-screens",
"version": "2.0.0-beta.8",
"description": "First incomplete navigation solution for your react-native app.",
"version": "2.0.0-beta.13",
"description": "Native navigation primitives for your React Native app.",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "npm run format && npm run lint && npm run test:unit",