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