Files
react-navigation/ios/RNSScreenContainer.m
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

223 lines
6.5 KiB
Objective-C

#import "RNSScreenContainer.h"
#import "RNSScreen.h"
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerObserverCoordinator.h>
#import <React/RCTUIManagerUtils.h>
@interface RNSScreenContainerManager : RCTViewManager
- (void)markUpdated:(RNSScreenContainerView *)screen;
@end
@interface RNSScreenContainerView () <RCTInvalidating>
@property (nonatomic, retain) UIViewController *controller;
@property (nonatomic, retain) NSMutableSet<RNSScreenView *> *activeScreens;
@property (nonatomic, retain) NSMutableArray<RNSScreenView *> *reactSubviews;
- (void)updateContainer;
@end
@implementation RNSScreenContainerView {
BOOL _needUpdate;
__weak RNSScreenContainerManager *_manager;
}
- (instancetype)initWithManager:(RNSScreenContainerManager *)manager
{
if (self = [super init]) {
_activeScreens = [NSMutableSet new];
_reactSubviews = [NSMutableArray new];
_controller = [[UIViewController alloc] init];
_needUpdate = NO;
_manager = manager;
[self addSubview:_controller.view];
}
return self;
}
- (void)markChildUpdated
{
// We want 'updateContainer' to be executed on main thread after all enqueued operations in
// uimanager are complete. For that we collect all marked containers in manager class and enqueue
// operation on ui thread that should run once all the updates are completed.
if (!_needUpdate) {
_needUpdate = YES;
[_manager markUpdated:self];
}
}
- (void)insertReactSubview:(RNSScreenView *)subview atIndex:(NSInteger)atIndex
{
subview.reactSuperview = self;
[_reactSubviews insertObject:subview atIndex:atIndex];
subview.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
}
- (void)removeReactSubview:(RNSScreenView *)subview
{
subview.reactSuperview = nil;
[_reactSubviews removeObject:subview];
}
- (NSArray<UIView *> *)reactSubviews
{
return _reactSubviews;
}
- (UIViewController *)reactViewController
{
return _controller;
}
- (void)detachScreen:(RNSScreenView *)screen
{
[screen.controller willMoveToParentViewController:nil];
[screen.controller.view removeFromSuperview];
[screen.controller removeFromParentViewController];
[_activeScreens removeObject:screen];
}
- (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];
}
- (void)updateContainer
{
_needUpdate = NO;
BOOL activeScreenRemoved = NO;
// remove screens that are no longer active
NSMutableSet *orphaned = [NSMutableSet setWithSet:_activeScreens];
for (RNSScreenView *screen in _reactSubviews) {
if (!screen.active && [_activeScreens containsObject:screen]) {
activeScreenRemoved = YES;
[self detachScreen:screen];
}
[orphaned removeObject:screen];
}
for (RNSScreenView *screen in orphaned) {
activeScreenRemoved = YES;
[self detachScreen:screen];
}
// detect if new screen is going to be activated
BOOL activeScreenAdded = NO;
for (RNSScreenView *screen in _reactSubviews) {
if (screen.active && ![_activeScreens containsObject:screen]) {
activeScreenAdded = YES;
}
}
// if we are adding new active screen, we perform remounting of all already marked as active
// this is done to mimick the effect UINavigationController has when willMoveToWindow:nil is
// triggered before the animation starts
if (activeScreenAdded) {
for (RNSScreenView *screen in _reactSubviews) {
if (screen.active && [_activeScreens containsObject:screen]) {
[self detachScreen:screen];
// disable interactions for the duration of transition
screen.userInteractionEnabled = NO;
}
}
// add new screens in order they are placed in subviews array
for (RNSScreenView *screen in _reactSubviews) {
if (screen.active) {
[self attachScreen:screen];
}
}
}
// if we are down to one active screen it means the transitioning is over and we want to notify
// the transition has finished
if ((activeScreenRemoved || activeScreenAdded) && _activeScreens.count == 1) {
RNSScreenView *singleActiveScreen = [_activeScreens anyObject];
// restore interactions
singleActiveScreen.userInteractionEnabled = YES;
[singleActiveScreen notifyFinishTransitioning];
}
if ((activeScreenRemoved || activeScreenAdded) && _controller.presentedViewController == nil) {
// if user has reachability enabled (one hand use) and the window is slided down the below
// method will force it to slide back up as it is expected to happen with UINavController when
// we push or pop views.
// We only do that if `presentedViewController` is nil, as otherwise it'd mean that modal has
// been presented on top of recently changed controller in which case the below method would
// dismiss such a modal (e.g., permission modal or alert)
[_controller dismissViewControllerAnimated:NO completion:nil];
}
}
- (void)didUpdateReactSubviews
{
[self markChildUpdated];
}
- (void)didMoveToWindow
{
if (self.window) {
[self reactAddControllerToClosestParent:_controller];
}
}
- (void)invalidate
{
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
}
- (void)layoutSubviews
{
[super layoutSubviews];
_controller.view.frame = self.bounds;
for (RNSScreenView *subview in _reactSubviews) {
subview.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
[subview setNeedsLayout];
}
}
@end
@implementation RNSScreenContainerManager {
NSMutableArray<RNSScreenContainerView *> *_markedContainers;
}
RCT_EXPORT_MODULE()
- (UIView *)view
{
if (!_markedContainers) {
_markedContainers = [NSMutableArray new];
}
return [[RNSScreenContainerView alloc] initWithManager:self];
}
- (void)markUpdated:(RNSScreenContainerView *)screen
{
RCTAssertMainQueue();
[_markedContainers addObject:screen];
if ([_markedContainers count] == 1) {
// we enqueue updates to be run on the main queue in order to make sure that
// all this updates (new screens attached etc) are executed in one batch
RCTExecuteOnMainQueue(^{
for (RNSScreenContainerView *container in _markedContainers) {
[container updateContainer];
}
[_markedContainers removeAllObjects];
});
}
}
@end