UNPKG

22.6 kBPlain TextView Raw
1/*
2 * Copyright (c) Facebook, Inc. and its 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 <React/RCTDevMenu.h>
9
10#import <FBReactNativeSpec/FBReactNativeSpec.h>
11#import <React/RCTBridge+Private.h>
12#import <React/RCTBundleURLProvider.h>
13#import <React/RCTDefines.h>
14#import <React/RCTDevSettings.h>
15#import <React/RCTKeyCommands.h>
16#import <React/RCTLog.h>
17#import <React/RCTReloadCommand.h>
18#import <React/RCTUtils.h>
19
20#import "CoreModulesPlugins.h"
21
22#if RCT_DEV_MENU
23
24#if RCT_ENABLE_INSPECTOR
25#import <React/RCTInspectorDevServerHelper.h>
26#endif
27
28NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
29
30@implementation UIWindow (RCTDevMenu)
31
32- (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event
33{
34 if (event.subtype == UIEventSubtypeMotionShake) {
35 [[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
36 }
37}
38
39@end
40
41@implementation RCTDevMenuItem {
42 RCTDevMenuItemTitleBlock _titleBlock;
43 dispatch_block_t _handler;
44}
45
46- (instancetype)initWithTitleBlock:(RCTDevMenuItemTitleBlock)titleBlock handler:(dispatch_block_t)handler
47{
48 if ((self = [super init])) {
49 _titleBlock = [titleBlock copy];
50 _handler = [handler copy];
51 }
52 return self;
53}
54
55RCT_NOT_IMPLEMENTED(-(instancetype)init)
56
57+ (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(dispatch_block_t)handler
58{
59 return [[self alloc] initWithTitleBlock:titleBlock handler:handler];
60}
61
62+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(dispatch_block_t)handler
63{
64 return [[self alloc]
65 initWithTitleBlock:^NSString * {
66 return title;
67 }
68 handler:handler];
69}
70
71- (void)callHandler
72{
73 if (_handler) {
74 _handler();
75 }
76}
77
78- (NSString *)title
79{
80 if (_titleBlock) {
81 return _titleBlock();
82 }
83 return nil;
84}
85
86@end
87
88typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action);
89
90@interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
91
92@end
93
94@implementation RCTDevMenu {
95 UIAlertController *_actionSheet;
96 NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
97}
98
99@synthesize bridge = _bridge;
100
101RCT_EXPORT_MODULE()
102
103+ (void)initialize
104{
105 // We're swizzling here because it's poor form to override methods in a category,
106 // however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
107 // no need to call the original implementation.
108 RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
109}
110
111+ (BOOL)requiresMainQueueSetup
112{
113 return YES;
114}
115
116- (instancetype)init
117{
118 if ((self = [super init])) {
119 [[NSNotificationCenter defaultCenter] addObserver:self
120 selector:@selector(showOnShake)
121 name:RCTShowDevMenuNotification
122 object:nil];
123 _extraMenuItems = [NSMutableArray new];
124
125#if TARGET_OS_SIMULATOR
126 RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
127 __weak __typeof(self) weakSelf = self;
128
129 // Toggle debug menu
130 [commands registerKeyCommandWithInput:@"d"
131 modifierFlags:UIKeyModifierCommand
132 action:^(__unused UIKeyCommand *command) {
133 [weakSelf toggle];
134 }];
135
136 // Toggle element inspector
137 [commands registerKeyCommandWithInput:@"i"
138 modifierFlags:UIKeyModifierCommand
139 action:^(__unused UIKeyCommand *command) {
140 [weakSelf.bridge.devSettings toggleElementInspector];
141 }];
142
143 // Reload in normal mode
144 [commands registerKeyCommandWithInput:@"n"
145 modifierFlags:UIKeyModifierCommand
146 action:^(__unused UIKeyCommand *command) {
147 [weakSelf.bridge.devSettings setIsDebuggingRemotely:NO];
148 }];
149#endif
150 }
151 return self;
152}
153
154- (dispatch_queue_t)methodQueue
155{
156 return dispatch_get_main_queue();
157}
158
159- (void)invalidate
160{
161 _presentedItems = nil;
162 [_actionSheet dismissViewControllerAnimated:YES
163 completion:^(void){}];
164}
165
166- (void)showOnShake
167{
168 if ([_bridge.devSettings isShakeToShowDevMenuEnabled]) {
169 [self show];
170 }
171}
172
173- (void)toggle
174{
175 if (_actionSheet) {
176 [_actionSheet dismissViewControllerAnimated:YES
177 completion:^(void){
178 }];
179 _actionSheet = nil;
180 } else {
181 [self show];
182 }
183}
184
185- (BOOL)isActionSheetShown
186{
187 return _actionSheet != nil;
188}
189
190- (void)addItem:(NSString *)title handler:(void (^)(void))handler
191{
192 [self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]];
193}
194
195- (void)addItem:(RCTDevMenuItem *)item
196{
197 [_extraMenuItems addObject:item];
198}
199
200- (void)setDefaultJSBundle
201{
202 [[RCTBundleURLProvider sharedSettings] resetToDefaults];
203 self->_bridge.bundleURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForFallbackResource:nil
204 fallbackExtension:nil];
205 RCTTriggerReloadCommandListeners(@"Dev menu - reset to default");
206}
207
208- (NSArray<RCTDevMenuItem *> *)_menuItemsToPresent
209{
210 NSMutableArray<RCTDevMenuItem *> *items = [NSMutableArray new];
211
212 // Add built-in items
213 __weak RCTBridge *bridge = _bridge;
214 __weak RCTDevSettings *devSettings = _bridge.devSettings;
215 __weak RCTDevMenu *weakSelf = self;
216
217 [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload"
218 handler:^{
219 RCTTriggerReloadCommandListeners(@"Dev menu - reload");
220 }]];
221
222 if (!devSettings.isProfilingEnabled) {
223 if (!devSettings.isRemoteDebuggingAvailable) {
224 [items
225 addObject:[RCTDevMenuItem
226 buttonItemWithTitle:@"Debugger Unavailable"
227 handler:^{
228 NSString *message = RCTTurboModuleEnabled()
229 ? @"Debugging is not currently supported when TurboModule is enabled."
230 : @"Include the RCTWebSocket library to enable JavaScript debugging.";
231 UIAlertController *alertController =
232 [UIAlertController alertControllerWithTitle:@"Debugger Unavailable"
233 message:message
234 preferredStyle:UIAlertControllerStyleAlert];
235 __weak __typeof__(alertController) weakAlertController = alertController;
236 [alertController
237 addAction:[UIAlertAction actionWithTitle:@"OK"
238 style:UIAlertActionStyleDefault
239 handler:^(__unused UIAlertAction *action) {
240 [weakAlertController
241 dismissViewControllerAnimated:YES
242 completion:nil];
243 }]];
244 [RCTPresentedViewController() presentViewController:alertController
245 animated:YES
246 completion:NULL];
247 }]];
248 } else {
249 [items addObject:[RCTDevMenuItem
250 buttonItemWithTitleBlock:^NSString * {
251 if (devSettings.isNuclideDebuggingAvailable) {
252 return devSettings.isDebuggingRemotely ? @"Stop Chrome Debugger" : @"Debug with Chrome";
253 } else {
254 return devSettings.isDebuggingRemotely ? @"Stop Debugging" : @"Debug";
255 }
256 }
257 handler:^{
258 devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely;
259 }]];
260 }
261
262 if (devSettings.isNuclideDebuggingAvailable && !devSettings.isDebuggingRemotely) {
263 [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Debug with Nuclide"
264 handler:^{
265#if RCT_ENABLE_INSPECTOR
266 [RCTInspectorDevServerHelper
267 attachDebugger:@"ReactNative"
268 withBundleURL:bridge.bundleURL
269 withView:RCTPresentedViewController()];
270#endif
271 }]];
272 }
273 }
274
275 [items addObject:[RCTDevMenuItem
276 buttonItemWithTitleBlock:^NSString * {
277 return devSettings.isElementInspectorShown ? @"Hide Inspector" : @"Show Inspector";
278 }
279 handler:^{
280 [devSettings toggleElementInspector];
281 }]];
282
283 if (devSettings.isHotLoadingAvailable) {
284 [items addObject:[RCTDevMenuItem
285 buttonItemWithTitleBlock:^NSString * {
286 // Previously known as "Hot Reloading". We won't use this term anymore.
287 return devSettings.isHotLoadingEnabled ? @"Disable Fast Refresh" : @"Enable Fast Refresh";
288 }
289 handler:^{
290 devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled;
291 }]];
292 }
293
294 if (devSettings.isLiveReloadAvailable) {
295 [items addObject:[RCTDevMenuItem
296 buttonItemWithTitleBlock:^NSString * {
297 return devSettings.isDebuggingRemotely
298 ? @"Systrace Unavailable"
299 : devSettings.isProfilingEnabled ? @"Stop Systrace" : @"Start Systrace";
300 }
301 handler:^{
302 if (devSettings.isDebuggingRemotely) {
303 UIAlertController *alertController =
304 [UIAlertController alertControllerWithTitle:@"Systrace Unavailable"
305 message:@"Stop debugging to enable Systrace."
306 preferredStyle:UIAlertControllerStyleAlert];
307 __weak __typeof__(alertController) weakAlertController = alertController;
308 [alertController
309 addAction:[UIAlertAction actionWithTitle:@"OK"
310 style:UIAlertActionStyleDefault
311 handler:^(__unused UIAlertAction *action) {
312 [weakAlertController
313 dismissViewControllerAnimated:YES
314 completion:nil];
315 }]];
316 [RCTPresentedViewController() presentViewController:alertController
317 animated:YES
318 completion:NULL];
319 } else {
320 devSettings.isProfilingEnabled = !devSettings.isProfilingEnabled;
321 }
322 }]];
323 // "Live reload" which refreshes on every edit was removed in favor of "Fast Refresh".
324 // While native code for "Live reload" is still there, please don't add the option back.
325 // See D15958697 for more context.
326 }
327
328 [items
329 addObject:[RCTDevMenuItem
330 buttonItemWithTitleBlock:^NSString * {
331 return @"Configure Bundler";
332 }
333 handler:^{
334 UIAlertController *alertController = [UIAlertController
335 alertControllerWithTitle:@"Configure Bundler"
336 message:@"Provide a custom bundler address, port, and entrypoint."
337 preferredStyle:UIAlertControllerStyleAlert];
338 [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
339 textField.placeholder = @"0.0.0.0";
340 }];
341 [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
342 textField.placeholder = @"8081";
343 }];
344 [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
345 textField.placeholder = @"index";
346 }];
347 [alertController
348 addAction:[UIAlertAction
349 actionWithTitle:@"Apply Changes"
350 style:UIAlertActionStyleDefault
351 handler:^(__unused UIAlertAction *action) {
352 NSArray *textfields = alertController.textFields;
353 UITextField *ipTextField = textfields[0];
354 UITextField *portTextField = textfields[1];
355 UITextField *bundleRootTextField = textfields[2];
356 NSString *bundleRoot = bundleRootTextField.text;
357 if (ipTextField.text.length == 0 && portTextField.text.length == 0) {
358 [weakSelf setDefaultJSBundle];
359 return;
360 }
361 NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
362 formatter.numberStyle = NSNumberFormatterDecimalStyle;
363 NSNumber *portNumber =
364 [formatter numberFromString:portTextField.text];
365 if (portNumber == nil) {
366 portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
367 }
368 [RCTBundleURLProvider sharedSettings].jsLocation = [NSString
369 stringWithFormat:@"%@:%d", ipTextField.text, portNumber.intValue];
370 __strong RCTBridge *strongBridge = bridge;
371 if (strongBridge) {
372 NSURL *bundleURL = bundleRoot.length
373 ? [[RCTBundleURLProvider sharedSettings]
374 jsBundleURLForBundleRoot:bundleRoot
375 fallbackResource:nil]
376 : [strongBridge.delegate sourceURLForBridge:strongBridge];
377 strongBridge.bundleURL = bundleURL;
378 RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
379 }
380 }]];
381 [alertController addAction:[UIAlertAction actionWithTitle:@"Reset to Default"
382 style:UIAlertActionStyleDefault
383 handler:^(__unused UIAlertAction *action) {
384 [weakSelf setDefaultJSBundle];
385 }]];
386 [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel"
387 style:UIAlertActionStyleCancel
388 handler:^(__unused UIAlertAction *action) {
389 return;
390 }]];
391 [RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL];
392 }]];
393
394 [items addObjectsFromArray:_extraMenuItems];
395 return items;
396}
397
398RCT_EXPORT_METHOD(show)
399{
400 if (_actionSheet || !_bridge || RCTRunningInAppExtension()) {
401 return;
402 }
403
404 NSString *bridgeDescription = _bridge.bridgeDescription;
405 NSString *description =
406 bridgeDescription.length > 0 ? [NSString stringWithFormat:@"Running %@", bridgeDescription] : nil;
407
408 // On larger devices we don't have an anchor point for the action sheet
409 UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone
410 ? UIAlertControllerStyleActionSheet
411 : UIAlertControllerStyleAlert;
412 _actionSheet = [UIAlertController alertControllerWithTitle:@"React Native Debug Menu"
413 message:description
414 preferredStyle:style];
415
416 NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
417 for (RCTDevMenuItem *item in items) {
418 [_actionSheet addAction:[UIAlertAction actionWithTitle:item.title
419 style:UIAlertActionStyleDefault
420 handler:[self alertActionHandlerForDevItem:item]]];
421 }
422
423 [_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel"
424 style:UIAlertActionStyleCancel
425 handler:[self alertActionHandlerForDevItem:nil]]];
426
427 _presentedItems = items;
428 [RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
429
430 [_bridge enqueueJSCall:@"RCTNativeAppEventEmitter"
431 method:@"emit"
432 args:@[@"RCTDevMenuShown"]
433 completion:NULL];
434}
435
436- (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item
437{
438 return ^(__unused UIAlertAction *action) {
439 if (item) {
440 [item callHandler];
441 }
442
443 self->_actionSheet = nil;
444 };
445}
446
447#pragma mark - deprecated methods and properties
448
449#define WARN_DEPRECATED_DEV_MENU_EXPORT() \
450 RCTLogWarn(@"Using deprecated method %s, use RCTDevSettings instead", __func__)
451
452- (void)setShakeToShow:(BOOL)shakeToShow
453{
454 _bridge.devSettings.isShakeToShowDevMenuEnabled = shakeToShow;
455}
456
457- (BOOL)shakeToShow
458{
459 return _bridge.devSettings.isShakeToShowDevMenuEnabled;
460}
461
462RCT_EXPORT_METHOD(reload)
463{
464 WARN_DEPRECATED_DEV_MENU_EXPORT();
465 RCTTriggerReloadCommandListeners(@"Unknown from JS");
466}
467
468RCT_EXPORT_METHOD(debugRemotely : (BOOL)enableDebug)
469{
470 WARN_DEPRECATED_DEV_MENU_EXPORT();
471 _bridge.devSettings.isDebuggingRemotely = enableDebug;
472}
473
474RCT_EXPORT_METHOD(setProfilingEnabled : (BOOL)enabled)
475{
476 WARN_DEPRECATED_DEV_MENU_EXPORT();
477 _bridge.devSettings.isProfilingEnabled = enabled;
478}
479
480- (BOOL)profilingEnabled
481{
482 return _bridge.devSettings.isProfilingEnabled;
483}
484
485RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled)
486{
487 WARN_DEPRECATED_DEV_MENU_EXPORT();
488 _bridge.devSettings.isHotLoadingEnabled = enabled;
489}
490
491- (BOOL)hotLoadingEnabled
492{
493 return _bridge.devSettings.isHotLoadingEnabled;
494}
495
496- (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
497{
498 return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(self, jsInvoker);
499}
500
501@end
502
503#else // Unavailable when not in dev mode
504
505@interface RCTDevMenu() <NativeDevMenuSpec>
506@end
507
508@implementation RCTDevMenu
509
510- (void)show
511{
512}
513- (void)reload
514{
515}
516- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler
517{
518}
519- (void)addItem:(RCTDevMenu *)item
520{
521}
522
523- (void)debugRemotely : (BOOL)enableDebug
524{
525}
526
527- (BOOL)isActionSheetShown
528{
529 return NO;
530}
531+ (NSString *)moduleName
532{
533 return @"DevMenu";
534}
535
536- (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
537{
538 return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(self, jsInvoker);
539}
540
541@end
542
543@implementation RCTDevMenuItem
544
545+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(void (^)(void))handler
546{
547 return nil;
548}
549+ (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(void (^)(void))handler
550{
551 return nil;
552}
553
554@end
555
556#endif
557
558@implementation RCTBridge (RCTDevMenu)
559
560- (RCTDevMenu *)devMenu
561{
562#if RCT_DEV_MENU
563 return [self moduleForClass:[RCTDevMenu class]];
564#else
565 return nil;
566#endif
567}
568
569@end
570
571Class RCTDevMenuCls(void) {
572 return RCTDevMenu.class;
573}