1#import "RNSScreenContainer.h" 2#import "RNSScreen.h" 3 4#ifdef RCT_NEW_ARCH_ENABLED 5#import <React/RCTConversions.h> 6#import <React/RCTFabricComponentsPlugins.h> 7#import <react/renderer/components/rnscreens/ComponentDescriptors.h> 8#import <react/renderer/components/rnscreens/Props.h> 9#endif 10 11@implementation RNScreensViewController 12 13#if !TARGET_OS_TV 14- (UIViewController *)childViewControllerForStatusBarStyle 15{ 16 return [self findActiveChildVC]; 17} 18 19- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation 20{ 21 return [self findActiveChildVC].preferredStatusBarUpdateAnimation; 22} 23 24- (UIViewController *)childViewControllerForStatusBarHidden 25{ 26 return [self findActiveChildVC]; 27} 28 29- (UIInterfaceOrientationMask)supportedInterfaceOrientations 30{ 31 return [self findActiveChildVC].supportedInterfaceOrientations; 32} 33 34- (UIViewController *)childViewControllerForHomeIndicatorAutoHidden 35{ 36 return [self findActiveChildVC]; 37} 38#endif 39 40- (UIViewController *)findActiveChildVC 41{ 42 for (UIViewController *childVC in self.childViewControllers) { 43 if ([childVC isKindOfClass:[RNSScreen class]] && 44 ((RNSScreen *)childVC).screenView.activityState == RNSActivityStateOnTop) { 45 return childVC; 46 } 47 } 48 return [[self childViewControllers] lastObject]; 49} 50 51@end 52 53@implementation RNSScreenContainerView { 54 BOOL _invalidated; 55 NSMutableSet *_activeScreens; 56} 57 58- (instancetype)init 59{ 60 if (self = [super init]) { 61#ifdef RCT_NEW_ARCH_ENABLED 62 static const auto defaultProps = std::make_shared<const facebook::react::RNSScreenContainerProps>(); 63 _props = defaultProps; 64#endif 65 _activeScreens = [NSMutableSet new]; 66 _reactSubviews = [NSMutableArray new]; 67 [self setupController]; 68 _invalidated = NO; 69 } 70 return self; 71} 72 73- (void)setupController 74{ 75 _controller = [[RNScreensViewController alloc] init]; 76 [self addSubview:_controller.view]; 77} 78 79- (void)markChildUpdated 80{ 81 // We want the attaching/detaching of children to be always made on main queue, which 82 // is currently true for `react-navigation` since this method is triggered 83 // by the changes of `Animated` value in stack's transition or adding/removing screens 84 // in all navigators 85 RCTAssertMainQueue(); 86 [self updateContainer]; 87} 88 89- (void)insertReactSubview:(RNSScreenView *)subview atIndex:(NSInteger)atIndex 90{ 91 subview.reactSuperview = self; 92 [_reactSubviews insertObject:subview atIndex:atIndex]; 93 [subview reactSetFrame:CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)]; 94} 95 96- (void)removeReactSubview:(RNSScreenView *)subview 97{ 98 subview.reactSuperview = nil; 99 [_reactSubviews removeObject:subview]; 100} 101 102- (NSArray<UIView *> *)reactSubviews 103{ 104 return _reactSubviews; 105} 106 107- (UIViewController *)reactViewController 108{ 109 return _controller; 110} 111 112- (UIViewController *)findChildControllerForScreen:(RNSScreenView *)screen 113{ 114 for (UIViewController *vc in _controller.childViewControllers) { 115 if (vc.view == screen) { 116 return vc; 117 } 118 } 119 return nil; 120} 121 122- (void)prepareDetach:(RNSScreenView *)screen 123{ 124 [[self findChildControllerForScreen:screen] willMoveToParentViewController:nil]; 125} 126 127- (void)detachScreen:(RNSScreenView *)screen 128{ 129 // We use findChildControllerForScreen method instead of directly accesing 130 // screen.controller because screen.controller may be reset to nil when the 131 // original screen view gets detached from the view hierarchy (we reset controller 132 // reference to avoid reference loops) 133 UIViewController *detachController = [self findChildControllerForScreen:screen]; 134 [detachController willMoveToParentViewController:nil]; 135 [screen removeFromSuperview]; 136 [detachController removeFromParentViewController]; 137 [_activeScreens removeObject:screen]; 138} 139 140- (void)attachScreen:(RNSScreenView *)screen atIndex:(NSInteger)index 141{ 142 [_controller addChildViewController:screen.controller]; 143 // the frame is already set at this moment because we adjust it in insertReactSubview. We don't 144 // want to update it here as react-driven animations may already run and hence changing the frame 145 // would result in visual glitches 146 [_controller.view insertSubview:screen.controller.view atIndex:index]; 147 [screen.controller didMoveToParentViewController:_controller]; 148 [_activeScreens addObject:screen]; 149} 150 151- (void)updateContainer 152{ 153 BOOL screenRemoved = NO; 154 // remove screens that are no longer active 155 NSMutableSet *orphaned = [NSMutableSet setWithSet:_activeScreens]; 156 for (RNSScreenView *screen in _reactSubviews) { 157 if (screen.activityState == RNSActivityStateInactive && [_activeScreens containsObject:screen]) { 158 screenRemoved = YES; 159 [self detachScreen:screen]; 160 } 161 [orphaned removeObject:screen]; 162 } 163 for (RNSScreenView *screen in orphaned) { 164 screenRemoved = YES; 165 [self detachScreen:screen]; 166 } 167 168 // detect if new screen is going to be activated 169 BOOL screenAdded = NO; 170 for (RNSScreenView *screen in _reactSubviews) { 171 if (screen.activityState != RNSActivityStateInactive && ![_activeScreens containsObject:screen]) { 172 screenAdded = YES; 173 } 174 } 175 176 if (screenAdded) { 177 // add new screens in order they are placed in subviews array 178 NSInteger index = 0; 179 for (RNSScreenView *screen in _reactSubviews) { 180 if (screen.activityState != RNSActivityStateInactive) { 181 if ([_activeScreens containsObject:screen] && screen.activityState == RNSActivityStateTransitioningOrBelowTop) { 182 // for screens that were already active we want to mimick the effect UINavigationController 183 // has when willMoveToWindow:nil is triggered before the animation starts 184 [self prepareDetach:screen]; 185 } else if (![_activeScreens containsObject:screen]) { 186 [self attachScreen:screen atIndex:index]; 187 } 188 index += 1; 189 } 190 } 191 } 192 193 for (RNSScreenView *screen in _reactSubviews) { 194 if (screen.activityState == RNSActivityStateOnTop) { 195 [screen notifyFinishTransitioning]; 196 } 197 } 198 199 if (screenRemoved || screenAdded) { 200 [self maybeDismissVC]; 201 } 202} 203 204- (void)maybeDismissVC 205{ 206 if (_controller.presentedViewController == nil && _controller.presentingViewController == nil) { 207 // if user has reachability enabled (one hand use) and the window is slided down the below 208 // method will force it to slide back up as it is expected to happen with UINavController when 209 // we push or pop views. 210 // We only do that if `presentedViewController` is nil, as otherwise it'd mean that modal has 211 // been presented on top of recently changed controller in which case the below method would 212 // dismiss such a modal (e.g., permission modal or alert) 213 [_controller dismissViewControllerAnimated:NO completion:nil]; 214 } 215} 216 217- (void)didUpdateReactSubviews 218{ 219 [self markChildUpdated]; 220} 221 222- (void)didMoveToWindow 223{ 224 if (self.window && !_invalidated) { 225 // We check whether the view has been invalidated before running side-effects in didMoveToWindow 226 // This is needed because when LayoutAnimations are used it is possible for view to be re-attached 227 // to a window despite the fact it has been removed from the React Native view hierarchy. 228 [self reactAddControllerToClosestParent:_controller]; 229 } 230} 231 232- (void)layoutSubviews 233{ 234 [super layoutSubviews]; 235 _controller.view.frame = self.bounds; 236 for (RNSScreenView *subview in _reactSubviews) { 237#ifdef RCT_NEW_ARCH_ENABLED 238 facebook::react::LayoutMetrics screenLayoutMetrics = subview.newLayoutMetrics; 239 screenLayoutMetrics.frame = RCTRectFromCGRect(CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)); 240 [subview updateLayoutMetrics:screenLayoutMetrics oldLayoutMetrics:subview.oldLayoutMetrics]; 241#else 242 [subview reactSetFrame:CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)]; 243#endif 244 [subview setNeedsLayout]; 245 } 246} 247 248#pragma mark-- Fabric specific 249#ifdef RCT_NEW_ARCH_ENABLED 250 251- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index 252{ 253 if (![childComponentView isKindOfClass:[RNSScreenView class]]) { 254 RCTLogError(@"ScreenContainer only accepts children of type Screen"); 255 return; 256 } 257 258 RNSScreenView *screenView = (RNSScreenView *)childComponentView; 259 260 RCTAssert( 261 childComponentView.reactSuperview == nil, 262 @"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)", 263 self, 264 childComponentView, 265 @(index), 266 @([childComponentView.superview tag])); 267 268 [_reactSubviews insertObject:screenView atIndex:index]; 269 screenView.reactSuperview = self; 270 facebook::react::LayoutMetrics screenLayoutMetrics = screenView.newLayoutMetrics; 271 screenLayoutMetrics.frame = RCTRectFromCGRect(CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)); 272 [screenView updateLayoutMetrics:screenLayoutMetrics oldLayoutMetrics:screenView.oldLayoutMetrics]; 273 [self markChildUpdated]; 274} 275 276- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index 277{ 278 RCTAssert( 279 childComponentView.reactSuperview == self, 280 @"Attempt to unmount a view which is mounted inside different view. (parent: %@, child: %@, index: %@)", 281 self, 282 childComponentView, 283 @(index)); 284 RCTAssert( 285 (_reactSubviews.count > index) && [_reactSubviews objectAtIndex:index] == childComponentView, 286 @"Attempt to unmount a view which has a different index. (parent: %@, child: %@, index: %@, actual index: %@, tag at index: %@)", 287 self, 288 childComponentView, 289 @(index), 290 @([_reactSubviews indexOfObject:childComponentView]), 291 @([[_reactSubviews objectAtIndex:index] tag])); 292 ((RNSScreenView *)childComponentView).reactSuperview = nil; 293 [_reactSubviews removeObject:childComponentView]; 294 [childComponentView removeFromSuperview]; 295 [self markChildUpdated]; 296} 297 298+ (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider 299{ 300 return facebook::react::concreteComponentDescriptorProvider<facebook::react::RNSScreenContainerComponentDescriptor>(); 301} 302 303- (void)prepareForRecycle 304{ 305 [super prepareForRecycle]; 306 [_controller willMoveToParentViewController:nil]; 307 [_controller removeFromParentViewController]; 308} 309 310#pragma mark-- Paper specific 311#else 312 313- (void)invalidate 314{ 315 _invalidated = YES; 316 [_controller willMoveToParentViewController:nil]; 317 [_controller removeFromParentViewController]; 318} 319 320#endif 321 322@end 323 324#ifdef RCT_NEW_ARCH_ENABLED 325Class<RCTComponentViewProtocol> RNSScreenContainerCls(void) 326{ 327 return RNSScreenContainerView.class; 328} 329#endif 330 331@implementation RNSScreenContainerManager 332 333RCT_EXPORT_MODULE() 334 335- (UIView *)view 336{ 337 return [[RNSScreenContainerView alloc] init]; 338} 339 340@end 341