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