#import "RCCNavigationController.h" #import "RCCViewController.h" #import "RCCManager.h" #import #import #if __has_include() #import #endif #import #import #import #import "RCCTitleViewHelper.h" #import "RCCCustomBarButtonItem.h" #import "UIViewController+Rotation.h" #import "RCTHelpers.h" #import "RCTConvert+UIBarButtonSystemItem.h" @implementation RCCNavigationController { BOOL _transitioning; NSMutableArray *_queuedViewControllers; } NSString const *CALLBACK_ASSOCIATED_KEY = @"RCCNavigationController.CALLBACK_ASSOCIATED_KEY"; NSString const *CALLBACK_ASSOCIATED_ID = @"RCCNavigationController.CALLBACK_ASSOCIATED_ID"; -(UIInterfaceOrientationMask)supportedInterfaceOrientations { return [self supportedControllerOrientations]; } - (instancetype)initWithProps:(NSDictionary *)props children:(NSArray *)children globalProps:(NSDictionary*)globalProps bridge:(RCTBridge *)bridge { _queuedViewControllers = [NSMutableArray new]; NSString *component = props[@"component"]; if (!component) return nil; NSDictionary *passProps = props[@"passProps"]; NSDictionary *navigatorStyle = props[@"style"]; RCCViewController *viewController = [[RCCViewController alloc] initWithComponent:component passProps:passProps navigatorStyle:navigatorStyle globalProps:globalProps bridge:bridge]; if (!viewController) return nil; viewController.controllerId = passProps[@"screenInstanceID"]; NSArray *leftButtons = props[@"leftButtons"]; if (leftButtons) { [self setButtons:leftButtons viewController:viewController side:@"left" animated:NO]; } NSArray *rightButtons = props[@"rightButtons"]; if (rightButtons) { [self setButtons:rightButtons viewController:viewController side:@"right" animated:NO]; } self = [super initWithRootViewController:viewController]; if (!self) return nil; self.delegate = self; self.navigationBar.translucent = NO; // default [self processTitleView:viewController props:props style:navigatorStyle]; [self setRotation:props]; NSArray* components = props[@"components"]; if (components.count) { for (NSDictionary* component in components) { NSMutableDictionary *mutableParams = [[NSMutableDictionary alloc] initWithDictionary:@{@"animated": @(0), @"component": component[@"screen"]}]; [mutableParams addEntriesFromDictionary:component]; [self performAction:@"push" actionParams:mutableParams bridge:bridge]; } } return self; } - (void)performAction:(NSString*)performAction actionParams:(NSDictionary*)actionParams bridge:(RCTBridge *)bridge { BOOL animated = actionParams[@"animated"] ? [actionParams[@"animated"] boolValue] : YES; // push if ([performAction isEqualToString:@"push"]) { NSString *component = actionParams[@"component"]; if (!component) return; NSMutableDictionary *passProps = [actionParams[@"passProps"] mutableCopy]; passProps[GLOBAL_SCREEN_ACTION_COMMAND_TYPE] = COMMAND_TYPE_PUSH; passProps[GLOBAL_SCREEN_ACTION_TIMESTAMP] = actionParams[GLOBAL_SCREEN_ACTION_TIMESTAMP]; NSDictionary *navigatorStyle = actionParams[@"style"]; NSNumber *keepStyleAcrossPush = [[RCCManager sharedInstance] getAppStyle][@"keepStyleAcrossPush"]; BOOL keepStyleAcrossPushBool = keepStyleAcrossPush ? [keepStyleAcrossPush boolValue] : YES; if (keepStyleAcrossPushBool) { if ([self.topViewController isKindOfClass:[RCCViewController class]]) { RCCViewController *parent = (RCCViewController*)self.topViewController; NSMutableDictionary *mergedStyle = [NSMutableDictionary dictionaryWithDictionary:parent.navigatorStyle]; // there are a few styles that we don't want to remember from our parent (they should be local) [mergedStyle removeObjectForKey:@"navBarHidden"]; [mergedStyle removeObjectForKey:@"statusBarHidden"]; [mergedStyle removeObjectForKey:@"navBarHideOnScroll"]; [mergedStyle removeObjectForKey:@"drawUnderNavBar"]; [mergedStyle removeObjectForKey:@"drawUnderTabBar"]; [mergedStyle removeObjectForKey:@"statusBarBlur"]; [mergedStyle removeObjectForKey:@"navBarBlur"]; [mergedStyle removeObjectForKey:@"navBarTranslucent"]; [mergedStyle removeObjectForKey:@"statusBarHideWithNavBar"]; [mergedStyle removeObjectForKey:@"autoAdjustScrollViewInsets"]; [mergedStyle removeObjectForKey:@"statusBarTextColorSchemeSingleScreen"]; [mergedStyle removeObjectForKey:@"disabledBackGesture"]; [mergedStyle removeObjectForKey:@"disabledSimultaneousGesture"]; [mergedStyle removeObjectForKey:@"navBarCustomView"]; [mergedStyle removeObjectForKey:@"navBarComponentAlignment"]; [mergedStyle addEntriesFromDictionary:navigatorStyle]; navigatorStyle = mergedStyle; } } RCCViewController *viewController = [[RCCViewController alloc] initWithComponent:component passProps:passProps navigatorStyle:navigatorStyle globalProps:nil bridge:bridge]; viewController.controllerId = passProps[@"screenInstanceID"]; [self processTitleView:viewController props:actionParams style:navigatorStyle]; NSString *backButtonTitle = actionParams[@"backButtonTitle"]; if (!backButtonTitle) { NSNumber *hideBackButtonTitle = [[RCCManager sharedInstance] getAppStyle][@"hideBackButtonTitle"]; BOOL hideBackButtonTitleBool = hideBackButtonTitle ? [hideBackButtonTitle boolValue] : NO; backButtonTitle = hideBackButtonTitleBool ? @"" : backButtonTitle; } if (backButtonTitle) { UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithTitle:backButtonTitle style:UIBarButtonItemStylePlain target:nil action:nil]; self.topViewController.navigationItem.backBarButtonItem = backItem; } else { self.topViewController.navigationItem.backBarButtonItem = nil; } NSNumber *backButtonHidden = actionParams[@"backButtonHidden"]; BOOL backButtonHiddenBool = backButtonHidden ? [backButtonHidden boolValue] : NO; if (backButtonHiddenBool) { viewController.navigationItem.hidesBackButton = YES; } NSArray *leftButtons = actionParams[@"leftButtons"]; if (leftButtons) { [self setButtons:leftButtons viewController:viewController side:@"left" animated:NO]; } NSArray *rightButtons = actionParams[@"rightButtons"]; if (rightButtons) { [self setButtons:rightButtons viewController:viewController side:@"right" animated:NO]; } NSArray *previewActions = actionParams[@"previewActions"]; NSString *previewViewID = actionParams[@"previewViewID"]; if (previewViewID) { if ([self.topViewController isKindOfClass:[RCCViewController class]]) { RCCViewController *topViewController = ((RCCViewController*)self.topViewController); topViewController.previewController = nil; [topViewController.navigationController unregisterForPreviewingWithContext:topViewController.previewContext]; viewController.previewActions = previewActions; viewController.previewCommit = actionParams[@"previewCommit"] ? [actionParams[@"previewCommit"] boolValue] : YES; NSNumber *previewHeight = actionParams[@"previewHeight"]; if (previewHeight) { viewController.preferredContentSize = CGSizeMake(viewController.view.frame.size.width, [previewHeight floatValue]); } if (topViewController.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) { dispatch_async(RCTGetUIManagerQueue(), ^{ [bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { UIView *view = viewRegistry[previewViewID]; topViewController.previewView = view; topViewController.previewContext = [topViewController registerForPreviewingWithDelegate:(id)topViewController sourceView:view]; }]; }); topViewController.previewController = viewController; } return; } } NSString *animationType = actionParams[@"animationType"]; if ([animationType isEqualToString:@"fade"]) { CATransition *transition = [CATransition animation]; transition.duration = 0.25; transition.type = kCATransitionFade; [self.view.layer addAnimation:transition forKey:kCATransition]; [self pushViewController:viewController animated:NO]; } else { [self pushViewController:viewController animated:animated]; } return; } // pop if ([performAction isEqualToString:@"pop"]) { NSString *animationType = actionParams[@"animationType"]; if ([animationType isEqualToString:@"fade"]) { CATransition *transition = [CATransition animation]; transition.duration = 0.25; transition.type = kCATransitionFade; [self.view.layer addAnimation:transition forKey:kCATransition]; [self popViewControllerAnimated:NO]; } else { [self popViewControllerAnimated:animated]; } return; } // popToRoot if ([performAction isEqualToString:@"popToRoot"]) { NSString *animationType = actionParams[@"animationType"]; if ([animationType isEqualToString:@"fade"]) { CATransition *transition = [CATransition animation]; transition.duration = 0.25; transition.type = kCATransitionFade; [self.view.layer addAnimation:transition forKey:kCATransition]; [self popToRootViewControllerAnimated:NO]; } else { [self popToRootViewControllerAnimated:animated]; } return; } // resetTo if ([performAction isEqualToString:@"resetTo"]) { NSArray *viewControllers; NSString *component = actionParams[@"component"]; NSArray *componentConfigs = actionParams[@"components"]; if (component) { NSMutableDictionary *passProps = [actionParams[@"passProps"] mutableCopy]; passProps[@"commandType"] = @"resetTo"; NSDictionary *style = actionParams[@"style"]; NSArray *leftButtons = actionParams[@"leftButtons"]; NSArray *rightButtons = actionParams[@"rightButtons"]; UIViewController *viewController = [self viewControllerWithComponent:component props:[passProps copy] style:style leftButtons:leftButtons rightButtons:rightButtons bridge:bridge]; NSDictionary *navigatorStyle = actionParams[@"style"]; [self processTitleView:viewController props:actionParams style:navigatorStyle]; viewControllers = @[viewController]; } else if (componentConfigs) { NSMutableArray *mutableViewControllers = [NSMutableArray arrayWithCapacity:[componentConfigs count]]; [componentConfigs enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull config, NSUInteger idx, BOOL * _Nonnull stop) { NSString *component = config[@"component"]; NSMutableDictionary *props = [config[@"passProps"] mutableCopy]; props[@"commandType"] = @"resetTo"; NSDictionary *style = config[@"style"]; NSArray *leftButtons = config[@"leftButtons"]; NSArray *rightButtons = config[@"rightButtons"]; UIViewController *viewController = [self viewControllerWithComponent:component props:[props copy] style:style leftButtons:leftButtons rightButtons:rightButtons bridge:bridge]; [self processTitleView:viewController props:actionParams style:style]; [mutableViewControllers addObject:viewController]; }]; viewControllers = [mutableViewControllers copy]; } if (!viewControllers) return; BOOL animated = actionParams[@"animated"] ? [actionParams[@"animated"] boolValue] : YES; NSString *animationType = actionParams[@"animationType"]; if ([animationType isEqualToString:@"fade"]) { CATransition *transition = [CATransition animation]; transition.duration = 0.25; transition.type = kCATransitionFade; [self.view.layer addAnimation:transition forKey:kCATransition]; [self setViewControllers:viewControllers animated:NO]; } else { [self setViewControllers:viewControllers animated:animated]; } return; } // setButtons if ([performAction isEqualToString:@"setButtons"]) { NSArray *buttons = actionParams[@"buttons"]; BOOL animated = actionParams[@"animated"] ? [actionParams[@"animated"] boolValue] : YES; NSString *side = actionParams[@"side"] ? actionParams[@"side"] : @"left"; [self setButtons:buttons viewController:self.topViewController side:side animated:animated]; return; } // setTitle if ([performAction isEqualToString:@"setTitle"] || [performAction isEqualToString:@"setTitleImage"]) { NSDictionary *navigatorStyle = actionParams[@"style"]; [self processTitleView:self.topViewController props:actionParams style:navigatorStyle]; return; } // toggleNavBar if ([performAction isEqualToString:@"setHidden"]) { NSNumber *animated = actionParams[@"animated"]; BOOL animatedBool = animated ? [animated boolValue] : YES; NSNumber *setHidden = actionParams[@"hidden"]; BOOL isHiddenBool = setHidden ? [setHidden boolValue] : NO; RCCViewController *topViewController = ((RCCViewController*)self.topViewController); topViewController.navigatorStyle[@"navBarHidden"] = setHidden; [topViewController setNavBarVisibilityChange:animatedBool]; } // setStyle if ([performAction isEqualToString:@"setStyle"]) { NSDictionary *navigatorStyle = actionParams; // merge the navigatorStyle of our parent if ([self.topViewController isKindOfClass:[RCCViewController class]]) { RCCViewController *parent = (RCCViewController*)self.topViewController; NSMutableDictionary *mergedStyle = [NSMutableDictionary dictionaryWithDictionary:parent.navigatorStyle]; // there are a few styles that we don't want to remember from our parent (they should be local) [mergedStyle setValuesForKeysWithDictionary:navigatorStyle]; navigatorStyle = mergedStyle; parent.navigatorStyle = navigatorStyle; [parent setStyleOnInit]; [parent updateStyle]; } } } - (UIViewController *)viewControllerWithComponent:(NSString *)component props:(NSDictionary *)props style:(NSDictionary *)style leftButtons:(NSArray *)leftButtons rightButtons:(NSArray *)rightButtons bridge:(RCTBridge *)bridge { RCCViewController *viewController = [[RCCViewController alloc] initWithComponent:component passProps:props navigatorStyle:style globalProps:nil bridge:bridge]; viewController.controllerId = props[@"screenInstanceID"]; viewController.navigationItem.hidesBackButton = YES; [self processTitleView:viewController props:props style:style]; if (leftButtons) { [self setButtons:leftButtons viewController:viewController side:@"left" animated:NO]; } if (rightButtons) { [self setButtons:rightButtons viewController:viewController side:@"right" animated:NO]; } return viewController; } -(void)onButtonPress:(UIBarButtonItem*)barButtonItem { NSString *callbackId = objc_getAssociatedObject(barButtonItem, &CALLBACK_ASSOCIATED_KEY); if (!callbackId) return; NSString *buttonId = objc_getAssociatedObject(barButtonItem, &CALLBACK_ASSOCIATED_ID); [[[RCCManager sharedInstance] getBridge].eventDispatcher sendAppEventWithName:callbackId body:@ { @"type": @"NavBarButtonPress", @"id": buttonId ? buttonId : [NSNull null] }]; } -(void)setButtons:(NSArray*)buttons viewController:(UIViewController*)viewController side:(NSString*)side animated:(BOOL)animated { NSMutableArray *barButtonItems = [NSMutableArray new]; for (NSDictionary *button in buttons) { NSString *title = button[@"title"]; UIImage *iconImage = nil; id icon = button[@"icon"]; if (icon) iconImage = [RCTConvert UIImage:icon]; NSString *__nullable component = button[@"component"]; NSString *__nullable systemItemName = button[@"systemItem"]; UIBarButtonSystemItem systemItem = [RCTConvert UIBarButtonSystemItem:systemItemName]; UIBarButtonItem *barButtonItem; if (iconImage) { barButtonItem = [[UIBarButtonItem alloc] initWithImage:iconImage style:UIBarButtonItemStylePlain target:self action:@selector(onButtonPress:)]; } else if (title) { barButtonItem = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:self action:@selector(onButtonPress:)]; NSMutableDictionary *buttonTextAttributes = [RCTHelpers textAttributesFromDictionary:button withPrefix:@"button"]; if (buttonTextAttributes.allKeys.count > 0) { [barButtonItem setTitleTextAttributes:buttonTextAttributes forState:UIControlStateNormal]; [barButtonItem setTitleTextAttributes:buttonTextAttributes forState:UIControlStateHighlighted]; } } else if (component) { RCTBridge *bridge = [[RCCManager sharedInstance] getBridge]; barButtonItem = [[RCCCustomBarButtonItem alloc] initWithComponentName:component passProps:button[@"passProps"] bridge:bridge]; } else if (systemItemName) { barButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:systemItem target:self action:@selector(onButtonPress:)]; } else continue; objc_setAssociatedObject(barButtonItem, &CALLBACK_ASSOCIATED_KEY, button[@"onPress"], OBJC_ASSOCIATION_RETAIN_NONATOMIC); [barButtonItems addObject:barButtonItem]; NSString *buttonId = button[@"id"]; if (buttonId) { objc_setAssociatedObject(barButtonItem, &CALLBACK_ASSOCIATED_ID, buttonId, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } NSNumber *disabled = button[@"disabled"]; BOOL disabledBool = disabled ? [disabled boolValue] : NO; if (disabledBool) { [barButtonItem setEnabled:NO]; } NSNumber *disableIconTintString = button[@"disableIconTint"]; BOOL disableIconTint = disableIconTintString ? [disableIconTintString boolValue] : NO; if (disableIconTint) { [barButtonItem setImage:[barButtonItem.image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]]; } if ([viewController isKindOfClass:[RCCViewController class]]) { RCCViewController *rccViewController = ((RCCViewController*)viewController); NSDictionary *navigatorStyle = rccViewController.navigatorStyle; id disabledButtonColor = navigatorStyle[@"disabledButtonColor"]; if (disabledButtonColor) { UIColor *color = [RCTConvert UIColor:disabledButtonColor]; [barButtonItem setTitleTextAttributes:@{NSForegroundColorAttributeName : color} forState:UIControlStateDisabled]; } } NSString *testID = button[@"testID"]; if (testID) { barButtonItem.accessibilityIdentifier = testID; } } if ([side isEqualToString:@"left"]) { [viewController.navigationItem setLeftBarButtonItems:barButtonItems animated:animated]; } if ([side isEqualToString:@"right"]) { [viewController.navigationItem setRightBarButtonItems:barButtonItems animated:animated]; } } -(void)processTitleView:(UIViewController*)viewController props:(NSDictionary*)props style:(NSDictionary*)style { BOOL isSetSubtitleBool = props[@"isSetSubtitle"] ? [props[@"isSetSubtitle"] boolValue] : NO; RCCTitleViewHelper *titleViewHelper = [[RCCTitleViewHelper alloc] init:viewController navigationController:self title:props[@"title"] subtitle:props[@"subtitle"] titleImageData:props[@"titleImage"] isSetSubtitle:isSetSubtitleBool]; [titleViewHelper setup:style]; } - (UIStatusBarStyle)preferredStatusBarStyle { return [self.topViewController preferredStatusBarStyle]; } - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { if(_transitioning) { NSDictionary *pushDetails =@{ @"viewController": viewController, @"animated": @(animated) }; [_queuedViewControllers addObject:pushDetails]; return; } _transitioning = YES; [super pushViewController:viewController animated:animated]; } #pragma mark - UINavigationControllerDelegate -(void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated { [viewController setNeedsStatusBarAppearanceUpdate]; } - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { dispatch_async(dispatch_get_main_queue(), ^{ _transitioning = NO; if ([_queuedViewControllers count] > 0) { NSDictionary *toPushDetails = [_queuedViewControllers firstObject]; [_queuedViewControllers removeObjectAtIndex:0]; [self pushViewController:toPushDetails[@"viewController"] animated:[toPushDetails[@"animated"] boolValue]]; } }); } @end