|
| 1 | +/* |
| 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + * |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + */ |
| 7 | + |
| 8 | +#import "RNTesterTabBarController.h" |
| 9 | + |
| 10 | +#if __has_include(<React/RCTReactNativeFactory.h>) |
| 11 | +#import <React/RCTReactNativeFactory.h> |
| 12 | +#else |
| 13 | +#import <RCTReactNativeFactory.h> |
| 14 | +#endif |
| 15 | + |
| 16 | +static NSString *const kScreenModuleName = @"RNTesterScreen"; |
| 17 | + |
| 18 | +@interface RNTesterScreenViewController : UIViewController |
| 19 | +@end |
| 20 | + |
| 21 | +@implementation RNTesterScreenViewController { |
| 22 | + RCTReactNativeFactory *_reactNativeFactory; |
| 23 | + NSDictionary *_initialProperties; |
| 24 | + NSDictionary *_launchOptions; |
| 25 | + NSURL *_documentationURL; |
| 26 | + BOOL _hidesChrome; |
| 27 | + NSLayoutConstraint *_rootViewBottomConstraint; |
| 28 | +} |
| 29 | + |
| 30 | +- (instancetype)initWithReactNativeFactory:(RCTReactNativeFactory *)reactNativeFactory |
| 31 | + title:(NSString *)title |
| 32 | + initialProperties:(NSDictionary *)initialProperties |
| 33 | + launchOptions:(NSDictionary *)launchOptions |
| 34 | + documentationURL:(NSURL *)documentationURL |
| 35 | + hidesChrome:(BOOL)hidesChrome |
| 36 | +{ |
| 37 | + if (self = [super initWithNibName:nil bundle:nil]) { |
| 38 | + _reactNativeFactory = reactNativeFactory; |
| 39 | + _initialProperties = initialProperties; |
| 40 | + _launchOptions = launchOptions; |
| 41 | + _documentationURL = documentationURL; |
| 42 | + _hidesChrome = hidesChrome; |
| 43 | + |
| 44 | + self.title = title; |
| 45 | + self.hidesBottomBarWhenPushed = hidesChrome; |
| 46 | + |
| 47 | + if (documentationURL != nil) { |
| 48 | + UIBarButtonItem *documentationItem = |
| 49 | + [[UIBarButtonItem alloc] initWithImage:[UIImage systemImageNamed:@"book"] |
| 50 | + style:UIBarButtonItemStylePlain |
| 51 | + target:self |
| 52 | + action:@selector(openDocumentation)]; |
| 53 | + documentationItem.accessibilityLabel = @"Documentation"; |
| 54 | + self.navigationItem.rightBarButtonItem = documentationItem; |
| 55 | + } |
| 56 | + } |
| 57 | + return self; |
| 58 | +} |
| 59 | + |
| 60 | +- (void)viewDidLoad |
| 61 | +{ |
| 62 | + [super viewDidLoad]; |
| 63 | + // Matches the screen's GroupedBackgroundColor, showing through outside the safe area. |
| 64 | + self.view.backgroundColor = [UIColor systemGroupedBackgroundColor]; |
| 65 | + |
| 66 | + // Draws the header's background behind the transparent navigation bar, spanning only the safe area's |
| 67 | + // width so it stays clear of side cutouts and a side tab bar. |
| 68 | + UIView *headerBackgroundView = [UIView new]; |
| 69 | + headerBackgroundView.backgroundColor = [UIColor systemBackgroundColor]; |
| 70 | + headerBackgroundView.translatesAutoresizingMaskIntoConstraints = NO; |
| 71 | + [self.view addSubview:headerBackgroundView]; |
| 72 | + |
| 73 | + // React Native scroll views don't adjust for bars or cutouts they sit under, so the surface is |
| 74 | + // kept clear of them. |
| 75 | + UIView *rootView = [_reactNativeFactory.rootViewFactory viewWithModuleName:kScreenModuleName |
| 76 | + initialProperties:_initialProperties |
| 77 | + launchOptions:_launchOptions |
| 78 | + bundleConfiguration:_reactNativeFactory.bundleConfiguration |
| 79 | + devMenuConfiguration:_reactNativeFactory.devMenuConfiguration]; |
| 80 | + rootView.translatesAutoresizingMaskIntoConstraints = NO; |
| 81 | + [self.view addSubview:rootView]; |
| 82 | + |
| 83 | + UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide; |
| 84 | + _rootViewBottomConstraint = [rootView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]; |
| 85 | + [NSLayoutConstraint activateConstraints:@[ |
| 86 | + [headerBackgroundView.topAnchor constraintEqualToAnchor:self.view.topAnchor], |
| 87 | + [headerBackgroundView.bottomAnchor constraintEqualToAnchor:safeArea.topAnchor], |
| 88 | + [headerBackgroundView.leadingAnchor constraintEqualToAnchor:safeArea.leadingAnchor], |
| 89 | + [headerBackgroundView.trailingAnchor constraintEqualToAnchor:safeArea.trailingAnchor], |
| 90 | + [rootView.topAnchor constraintEqualToAnchor:safeArea.topAnchor], |
| 91 | + _rootViewBottomConstraint, |
| 92 | + [rootView.leadingAnchor constraintEqualToAnchor:safeArea.leadingAnchor], |
| 93 | + [rootView.trailingAnchor constraintEqualToAnchor:safeArea.trailingAnchor], |
| 94 | + ]]; |
| 95 | +} |
| 96 | + |
| 97 | +- (void)viewSafeAreaInsetsDidChange |
| 98 | +{ |
| 99 | + [super viewSafeAreaInsetsDidChange]; |
| 100 | + // The window's own bottom inset (the home indicator) only applies beneath a bottom tab bar, which |
| 101 | + // already clears it. With the tab bar at the side or hidden, the surface runs to the bottom edge. |
| 102 | + CGFloat bottomInset = self.view.safeAreaInsets.bottom; |
| 103 | + BOOL hasBottomBar = bottomInset > self.view.window.safeAreaInsets.bottom; |
| 104 | + _rootViewBottomConstraint.constant = hasBottomBar ? -bottomInset : 0; |
| 105 | +} |
| 106 | + |
| 107 | +- (void)viewWillAppear:(BOOL)animated |
| 108 | +{ |
| 109 | + [super viewWillAppear:animated]; |
| 110 | + [self.navigationController setNavigationBarHidden:_hidesChrome animated:animated]; |
| 111 | +} |
| 112 | + |
| 113 | +- (void)openDocumentation |
| 114 | +{ |
| 115 | + [[UIApplication sharedApplication] openURL:_documentationURL options:@{} completionHandler:nil]; |
| 116 | +} |
| 117 | + |
| 118 | +@end |
| 119 | + |
| 120 | +@implementation RNTesterTabBarController { |
| 121 | + RCTReactNativeFactory *_reactNativeFactory; |
| 122 | + UINavigationController *_componentsNavigationController; |
| 123 | +} |
| 124 | + |
| 125 | +- (instancetype)initWithReactNativeFactory:(RCTReactNativeFactory *)reactNativeFactory |
| 126 | + initialProperties:(NSDictionary *)initialProperties |
| 127 | + launchOptions:(NSDictionary *)launchOptions |
| 128 | +{ |
| 129 | + if (self = [super initWithNibName:nil bundle:nil]) { |
| 130 | + _reactNativeFactory = reactNativeFactory; |
| 131 | + |
| 132 | + _componentsNavigationController = [self navigationControllerForScreen:@"components" |
| 133 | + title:@"Components" |
| 134 | + image:@"square.stack.3d.up" |
| 135 | + testID:@"components-tab" |
| 136 | + initialProperties:initialProperties |
| 137 | + launchOptions:launchOptions]; |
| 138 | + self.viewControllers = @[ |
| 139 | + _componentsNavigationController, |
| 140 | + [self navigationControllerForScreen:@"apis" |
| 141 | + title:@"APIs" |
| 142 | + image:@"curlybraces" |
| 143 | + testID:@"apis-tab" |
| 144 | + initialProperties:initialProperties |
| 145 | + launchOptions:launchOptions], |
| 146 | + [self navigationControllerForScreen:@"playgrounds" |
| 147 | + title:@"Playground" |
| 148 | + image:@"play.rectangle" |
| 149 | + testID:@"playground-tab" |
| 150 | + initialProperties:initialProperties |
| 151 | + launchOptions:launchOptions], |
| 152 | + ]; |
| 153 | + } |
| 154 | + return self; |
| 155 | +} |
| 156 | + |
| 157 | +- (UINavigationController *)navigationControllerForScreen:(NSString *)screen |
| 158 | + title:(NSString *)title |
| 159 | + image:(NSString *)imageName |
| 160 | + testID:(NSString *)testID |
| 161 | + initialProperties:(NSDictionary *)initialProperties |
| 162 | + launchOptions:(NSDictionary *)launchOptions |
| 163 | +{ |
| 164 | + NSMutableDictionary *screenProperties = [initialProperties mutableCopy]; |
| 165 | + screenProperties[@"screen"] = screen; |
| 166 | + |
| 167 | + RNTesterScreenViewController *rootViewController = |
| 168 | + [[RNTesterScreenViewController alloc] initWithReactNativeFactory:_reactNativeFactory |
| 169 | + title:title |
| 170 | + initialProperties:screenProperties |
| 171 | + launchOptions:launchOptions |
| 172 | + documentationURL:nil |
| 173 | + hidesChrome:NO]; |
| 174 | + UINavigationController *navigationController = |
| 175 | + [[UINavigationController alloc] initWithRootViewController:rootViewController]; |
| 176 | + // Each screen draws the header's background itself, see RNTesterScreenViewController. |
| 177 | + UINavigationBarAppearance *navigationBarAppearance = [UINavigationBarAppearance new]; |
| 178 | + [navigationBarAppearance configureWithTransparentBackground]; |
| 179 | + navigationController.navigationBar.standardAppearance = navigationBarAppearance; |
| 180 | + navigationController.navigationBar.scrollEdgeAppearance = navigationBarAppearance; |
| 181 | + navigationController.tabBarItem = [[UITabBarItem alloc] initWithTitle:title |
| 182 | + image:[UIImage systemImageNamed:imageName] |
| 183 | + tag:0]; |
| 184 | + navigationController.tabBarItem.accessibilityIdentifier = testID; |
| 185 | + return navigationController; |
| 186 | +} |
| 187 | + |
| 188 | +- (void)pushRoute:(NSDictionary *)route |
| 189 | +{ |
| 190 | + auto *navigationController = (UINavigationController *)self.selectedViewController; |
| 191 | + [navigationController pushViewController:[self viewControllerForRoute:route] animated:YES]; |
| 192 | +} |
| 193 | + |
| 194 | +- (void)pushRouteFromDeepLink:(NSDictionary *)route |
| 195 | +{ |
| 196 | + self.selectedViewController = _componentsNavigationController; |
| 197 | + [_componentsNavigationController popToRootViewControllerAnimated:NO]; |
| 198 | + [_componentsNavigationController pushViewController:[self viewControllerForRoute:route] animated:NO]; |
| 199 | +} |
| 200 | + |
| 201 | +- (UIViewController *)viewControllerForRoute:(NSDictionary *)route |
| 202 | +{ |
| 203 | + NSMutableDictionary *screenProperties = [NSMutableDictionary dictionary]; |
| 204 | + screenProperties[@"moduleKey"] = route[@"moduleKey"]; |
| 205 | + if ([route[@"exampleKey"] isKindOfClass:[NSString class]]) { |
| 206 | + screenProperties[@"exampleKey"] = route[@"exampleKey"]; |
| 207 | + } |
| 208 | + |
| 209 | + NSURL *documentationURL = nil; |
| 210 | + if ([route[@"documentationURL"] isKindOfClass:[NSString class]]) { |
| 211 | + documentationURL = [NSURL URLWithString:route[@"documentationURL"]]; |
| 212 | + } |
| 213 | + |
| 214 | + return [[RNTesterScreenViewController alloc] initWithReactNativeFactory:_reactNativeFactory |
| 215 | + title:route[@"title"] |
| 216 | + initialProperties:screenProperties |
| 217 | + launchOptions:nil |
| 218 | + documentationURL:documentationURL |
| 219 | + hidesChrome:[route[@"hidesChrome"] boolValue]]; |
| 220 | +} |
| 221 | + |
| 222 | +@end |
0 commit comments