-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ios] Orphaned view remains after (react native's) modal dismissed #2538
Comments
Issue validatorThe issue is valid! |
any progress on that?, |
I can confirm problem is present on |
Observation which maybe will be helpful. After setting Modal's animationType to |
As for the quick solution, you can check if replacing the code of your /*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTModalHostView.h"
#import <UIKit/UIKit.h>
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTModalHostViewController.h"
#import "RCTTouchHandler.h"
#import "RCTUIManager.h"
#import "RCTUtils.h"
#import "UIView+React.h"
@implementation RCTModalHostView {
__weak RCTBridge *_bridge;
BOOL _isPresented;
BOOL _invalidated;
RCTModalHostViewController *_modalViewController;
RCTTouchHandler *_touchHandler;
UIView *_reactSubview;
UIInterfaceOrientation _lastKnownOrientation;
RCTDirectEventBlock _onRequestClose;
}
RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame)
RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : coder)
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super initWithFrame:CGRectZero])) {
_bridge = bridge;
_modalViewController = [RCTModalHostViewController new];
UIView *containerView = [UIView new];
containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
_modalViewController.view = containerView;
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:bridge];
_isPresented = NO;
__weak typeof(self) weakSelf = self;
_modalViewController.boundsDidChangeBlock = ^(CGRect newBounds) {
[weakSelf notifyForBoundsChange:newBounds];
};
}
return self;
}
- (void)notifyForBoundsChange:(CGRect)newBounds
{
if (_reactSubview && _isPresented) {
[_bridge.uiManager setSize:newBounds.size forView:_reactSubview];
[self notifyForOrientationChange];
}
}
- (void)setOnRequestClose:(RCTDirectEventBlock)onRequestClose
{
_onRequestClose = onRequestClose;
}
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)controller
{
if (_onRequestClose != nil) {
_onRequestClose(nil);
}
}
- (void)notifyForOrientationChange
{
if (!_onOrientationChange) {
return;
}
UIInterfaceOrientation currentOrientation = [RCTSharedApplication() statusBarOrientation];
if (currentOrientation == _lastKnownOrientation) {
return;
}
_lastKnownOrientation = currentOrientation;
BOOL isPortrait = currentOrientation == UIInterfaceOrientationPortrait ||
currentOrientation == UIInterfaceOrientationPortraitUpsideDown;
NSDictionary *eventPayload = @{
@"orientation" : isPortrait ? @"portrait" : @"landscape",
};
_onOrientationChange(eventPayload);
}
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
{
RCTAssert(_reactSubview == nil, @"Modal view can only have one subview");
[super insertReactSubview:subview atIndex:atIndex];
[_touchHandler attachToView:subview];
[_modalViewController.view insertSubview:subview atIndex:0];
_reactSubview = subview;
}
- (void)removeReactSubview:(UIView *)subview
{
RCTAssert(subview == _reactSubview, @"Cannot remove view other than modal view");
// Superclass (category) removes the `subview` from actual `superview`.
[super removeReactSubview:subview];
[_touchHandler detachFromView:subview];
_reactSubview = nil;
}
- (void)didUpdateReactSubviews
{
// Do nothing, as subview (singular) is managed by `insertReactSubview:atIndex:`
}
- (void)dismissModalViewController
{
if (_isPresented) {
[_delegate dismissModalHostView:self withViewController:_modalViewController animated:[self hasAnimationType]];
_isPresented = NO;
}
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
// In the case where there is a LayoutAnimation, we will be reinserted into the view hierarchy but only for aesthetic
// purposes. In such a case, we should NOT represent the <Modal>.
if (!self.userInteractionEnabled && ![self.superview.reactSubviews containsObject:self]) {
return;
}
[self ensurePresentedOnlyIfNeeded];
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
[self ensurePresentedOnlyIfNeeded];
}
- (void)invalidate
{
_invalidated = YES;
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissModalViewController];
});
}
- (BOOL)isTransparent
{
return _modalViewController.modalPresentationStyle == UIModalPresentationOverFullScreen;
}
- (BOOL)hasAnimationType
{
return ![self.animationType isEqualToString:@"none"];
}
- (void)setVisible:(BOOL)visible
{
if (_visible != visible) {
_visible = visible;
[self ensurePresentedOnlyIfNeeded];
}
}
- (void)ensurePresentedOnlyIfNeeded
{
BOOL shouldBePresented = !_isPresented && _visible && self.window && !_invalidated;
if (shouldBePresented) {
RCTAssert(self.reactViewController, @"Can't present modal view controller without a presenting view controller");
_modalViewController.supportedInterfaceOrientations = [self supportedOrientationsMask];
if ([self.animationType isEqualToString:@"fade"]) {
_modalViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
} else if ([self.animationType isEqualToString:@"slide"]) {
_modalViewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
}
if (self.presentationStyle != UIModalPresentationNone) {
_modalViewController.modalPresentationStyle = self.presentationStyle;
}
if (@available(iOS 13.0, *)) {
_modalViewController.presentationController.delegate = self;
}
[_delegate presentModalHostView:self withViewController:_modalViewController animated:[self hasAnimationType]];
_isPresented = YES;
}
BOOL shouldBeHidden = _isPresented && (!_visible || !self.superview);
if (shouldBeHidden) {
[self dismissModalViewController];
}
}
- (void)setTransparent:(BOOL)transparent
{
if (self.isTransparent != transparent) {
return;
}
_modalViewController.modalPresentationStyle =
transparent ? UIModalPresentationOverFullScreen : UIModalPresentationFullScreen;
}
- (UIInterfaceOrientationMask)supportedOrientationsMask
{
if (_supportedOrientations.count == 0) {
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
} else {
return UIInterfaceOrientationMaskPortrait;
}
}
UIInterfaceOrientationMask supportedOrientations = 0;
for (NSString *orientation in _supportedOrientations) {
if ([orientation isEqualToString:@"portrait"]) {
supportedOrientations |= UIInterfaceOrientationMaskPortrait;
} else if ([orientation isEqualToString:@"portrait-upside-down"]) {
supportedOrientations |= UIInterfaceOrientationMaskPortraitUpsideDown;
} else if ([orientation isEqualToString:@"landscape"]) {
supportedOrientations |= UIInterfaceOrientationMaskLandscape;
} else if ([orientation isEqualToString:@"landscape-left"]) {
supportedOrientations |= UIInterfaceOrientationMaskLandscapeLeft;
} else if ([orientation isEqualToString:@"landscape-right"]) {
supportedOrientations |= UIInterfaceOrientationMaskLandscapeRight;
}
}
return supportedOrientations;
}
@end solves the issue. Above code checks if the view has already been invalidated, and if so, it does not permit reattaching the modal. |
I made a PR trying to solve this problem: #2581. Can you check if applying it fixes the issues and does not introduce any new ones? |
@WoLewicki I have tested the change to ios/LayoutReanimation/REAUIManager.mm as committed with #2581 using the exact setup described in this issue's description above. I applied the changes (locally) to both In both cases the Modal properly clears (visually and in the view hierarchy). I haven't noticed additional problems with the simple app. Looks good so far. Nice work! (If I get a chance, I'll also check against a more complicated production application I have) |
@WoLewicki I tested the change in my application and it fixes the modal issue. But I still have same issue occurring on back navigation (using react-navigation 6). Strangely this only occurs when I'm navigating back from a view with longer scrollable content. |
I can show you the view hierarchy Maybe it's related to #2460 but it occurs on iOS and seems to be relevant to this issue. I will try to create a reproduction in new project. Unfortunately I cannot share my client's code where this issue is occurring. |
Unfortunately I cannot say much without a reproduction, the snapshot of the view hierarchy does not tell enough to conclude anything 😕 . Should the |
Working on the reproduction. The RNSScreenView shouldn't be there. It's just a transparent screen over the content screen. |
@WoLewicki I have created a reproduction here https://github.com/Sanglepp/AnimatedIssueDemo/tree/master Also here is video attached. (In video the changes to ios/LayoutReanimation/REAUIManager.mm have been applied) Reproduction.movThe RNSScreenView is added on on top when navigating back from checkout. |
Looks like this is happening both when navigating back and if you show/hide modal from the screen which uses scroll view.. in my case, when I hide modal, it hides, and then another empty one is like "injected" and it blocks any further interaction with app, it's that UITransition view.. |
Is there any progress on this? I would be happy to help out if possible as this is a blocker issue in my app. |
Probably fixed with this PR - #2581 but I need to check it. |
This fixes the modal issue but the back navigation issue still persists. Happens on a screen with longer scrollview. |
Fixed with - #2581 |
HI! @piaskowyk any ideas when this is going to be released? Thanks! |
Description
Once a RN Modal has been displayed, a "zombie"
UITransitionView
(with children) is left-over after the modal's dismissal. Depending upon the modal's transparency, the zombie view may or may not visually obscure the entire screen. In either case this view will "swallow" all touch events rendering the app unusable thereafter.Note
This issue has been making the rounds in several repos lately, and it appears
react-native-reanimated
is common among several reports. It is also being referenced in other issues in this repository, but I wanted to create a separate issue to elevate visibility on a minimal repro case, as the others include several other deps and may have unrelated problems.Expected behavior
React native modals are dismissed without leaving behind orphaned (UIKit) views.
Actual behavior & steps to reproduce
When
react-native-reanimated
is present (package.json + pod installed), modals do not dismiss properly. More specifically, the dismissal animation appears to work properly, but there are left over (UIKit) views/controllers on the display hierarchy which obscure the rest of the screen and swallow touch events preventing further interaction.Minimal working steps
npx react-native init Foo
App.js
contents with the canonical Modal example for RN 0.66 hereTo Experience the issue
starting after completing working steps above
yarn/npm
installreact-native-reanimated@2.3.0-beta.2
other versions may also apply; issue presents with reanimated2 and RN 0.66pod install
Capture View Hierarchy
in XCode and observe the left over orphaned UIKit views.Snack or minimal code example
snack does not reproduce the issue
App.js
Taken directly from react native example page
Package versions
From
package.json
Affected platforms
The text was updated successfully, but these errors were encountered: