UNPKG

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