From 2f9bd1f62fb5a083fb6b29b0bb43197d48cca520 Mon Sep 17 00:00:00 2001 From: Yusef Napora Date: Tue, 22 Sep 2015 10:43:56 -0700 Subject: [PATCH] App Extension support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This adds workarounds for the code that was preventing React from compiling when linked against an iOS App Extension target. Some iOS APIs are unavailable to App Extensions, and Xcode's static analysis will catch attempts to use methods that have been flagged as unavailable. React currently uses two APIs that are off limits to extensions: `[UIApplication sharedApplication]` and `[UIAlertView initWith ...]`. This commit adds a helper function to `RCTUtils.[hm]` called `RCTRunningInAppExtension()`, which returns `YES` if, at runtime, it can be determined that we're running in an app extension (by checking whether the path to `[NSBundle mainBundle]` has the `"appex"` path extension). It also adds a `RCTSharedApplication()` function, which will return `nil` if running in an App Extension. If running in an App, `RCTSharedApplication()` calls `sharedApplication` by calling `performSelector:` on the `UIApplication` class. This passes the static analysis check, and, in my opinion, obeys the "spirit of th Closes https://github.com/facebook/react-native/pull/1895 Reviewed By: @​svcscm Differential Revision: D2224128 Pulled By: @nicklockwood --- .../ActionSheetIOS/RCTActionSheetManager.m | 16 +++++++-- Libraries/CameraRoll/RCTImagePickerManager.m | 20 ++++++++--- Libraries/LinkingIOS/RCTLinkingManager.m | 10 ++++-- .../RCTPushNotificationManager.m | 36 +++++++++++-------- React/Base/RCTPerfStats.m | 7 +++- React/Base/RCTUtils.h | 11 ++++++ React/Base/RCTUtils.m | 36 +++++++++++++++++++ React/Modules/RCTAlertManager.m | 14 ++++---- React/Modules/RCTAppState.m | 7 +++- React/Modules/RCTDevMenu.m | 11 +++--- React/Modules/RCTRedBox.m | 2 +- React/Modules/RCTStatusBarManager.m | 11 +++--- 12 files changed, 135 insertions(+), 46 deletions(-) diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.m b/Libraries/ActionSheetIOS/RCTActionSheetManager.m index 34a791520..dbaa9affd 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheetManager.m +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.m @@ -43,6 +43,11 @@ RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options failureCallback:(__unused RCTResponseSenderBlock)failureCallback successCallback:(RCTResponseSenderBlock)successCallback) { + if (RCTRunningInAppExtension()) { + RCTLogError(@"Unable to show action sheet from app extension"); + return; + } + UIActionSheet *actionSheet = [UIActionSheet new]; actionSheet.title = options[@"title"]; @@ -62,7 +67,7 @@ RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options _callbacks[RCTKeyForInstance(actionSheet)] = successCallback; - UIWindow *appWindow = [UIApplication sharedApplication].delegate.window; + UIWindow *appWindow = RCTSharedApplication().delegate.window; if (appWindow == nil) { RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options); return; @@ -87,8 +92,13 @@ RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options failureCallback(@[@"No `url` or `message` to share"]); return; } + if (RCTRunningInAppExtension()) { + failureCallback(@[@"Unable to show action sheet from app extension"]); + return; + } + UIActivityViewController *share = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; - UIViewController *ctrl = [UIApplication sharedApplication].delegate.window.rootViewController; + UIViewController *ctrl = RCTSharedApplication().delegate.window.rootViewController; #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 @@ -146,7 +156,7 @@ RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options RCTLogWarn(@"No callback registered for action sheet: %@", actionSheet.title); } - [[UIApplication sharedApplication].delegate.window makeKeyWindow]; + [RCTSharedApplication().delegate.window makeKeyWindow]; } #pragma mark Private diff --git a/Libraries/CameraRoll/RCTImagePickerManager.m b/Libraries/CameraRoll/RCTImagePickerManager.m index 00a28a665..281149196 100644 --- a/Libraries/CameraRoll/RCTImagePickerManager.m +++ b/Libraries/CameraRoll/RCTImagePickerManager.m @@ -10,6 +10,8 @@ #import "RCTImagePickerManager.h" #import "RCTRootView.h" +#import "RCTLog.h" +#import "RCTUtils.h" #import @@ -53,7 +55,12 @@ RCT_EXPORT_METHOD(openCameraDialog:(NSDictionary *)config successCallback:(RCTResponseSenderBlock)callback cancelCallback:(RCTResponseSenderBlock)cancelCallback) { - UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; + if (RCTRunningInAppExtension()) { + cancelCallback(@[@"Camera access is unavailable in an app extension"]); + return; + } + + UIWindow *keyWindow = RCTSharedApplication().keyWindow; UIViewController *rootViewController = keyWindow.rootViewController; UIImagePickerController *imagePicker = [UIImagePickerController new]; @@ -75,7 +82,12 @@ RCT_EXPORT_METHOD(openSelectDialog:(NSDictionary *)config successCallback:(RCTResponseSenderBlock)callback cancelCallback:(RCTResponseSenderBlock)cancelCallback) { - UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; + if (RCTRunningInAppExtension()) { + cancelCallback(@[@"Image picker is currently unavailable in an app extension"]); + return; + } + + UIWindow *keyWindow = RCTSharedApplication().keyWindow; UIViewController *rootViewController = keyWindow.rootViewController; UIImagePickerController *imagePicker = [UIImagePickerController new]; @@ -109,7 +121,7 @@ didFinishPickingMediaWithInfo:(NSDictionary *)info [_pickerCallbacks removeObjectAtIndex:index]; [_pickerCancelCallbacks removeObjectAtIndex:index]; - UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; + UIWindow *keyWindow = RCTSharedApplication().keyWindow; UIViewController *rootViewController = keyWindow.rootViewController; [rootViewController dismissViewControllerAnimated:YES completion:nil]; @@ -125,7 +137,7 @@ didFinishPickingMediaWithInfo:(NSDictionary *)info [_pickerCallbacks removeObjectAtIndex:index]; [_pickerCancelCallbacks removeObjectAtIndex:index]; - UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; + UIWindow *keyWindow = RCTSharedApplication().keyWindow; UIViewController *rootViewController = keyWindow.rootViewController; [rootViewController dismissViewControllerAnimated:YES completion:nil]; diff --git a/Libraries/LinkingIOS/RCTLinkingManager.m b/Libraries/LinkingIOS/RCTLinkingManager.m index af1d41752..37a2cc4d6 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.m +++ b/Libraries/LinkingIOS/RCTLinkingManager.m @@ -58,14 +58,20 @@ RCT_EXPORT_MODULE() RCT_EXPORT_METHOD(openURL:(NSURL *)URL) { // Doesn't really matter what thread we call this on since it exits the app - [[UIApplication sharedApplication] openURL:URL]; + [RCTSharedApplication() openURL:URL]; } RCT_EXPORT_METHOD(canOpenURL:(NSURL *)URL callback:(RCTResponseSenderBlock)callback) { + if (RCTRunningInAppExtension()) { + // Technically Today widgets can open urls, but supporting that would require + // a reference to the NSExtensionContext + callback(@[@(NO)]); + } + // This can be expensive, so we deliberately don't call on main thread - BOOL canOpen = [[UIApplication sharedApplication] canOpenURL:URL]; + BOOL canOpen = [RCTSharedApplication() canOpenURL:URL]; callback(@[@(canOpen)]); } diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index fda9980f9..6af5f8e91 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -122,7 +122,7 @@ RCT_EXPORT_MODULE() */ RCT_EXPORT_METHOD(setApplicationIconBadgeNumber:(NSInteger)number) { - [UIApplication sharedApplication].applicationIconBadgeNumber = number; + RCTSharedApplication().applicationIconBadgeNumber = number; } /** @@ -131,12 +131,16 @@ RCT_EXPORT_METHOD(setApplicationIconBadgeNumber:(NSInteger)number) RCT_EXPORT_METHOD(getApplicationIconBadgeNumber:(RCTResponseSenderBlock)callback) { callback(@[ - @([UIApplication sharedApplication].applicationIconBadgeNumber) + @(RCTSharedApplication().applicationIconBadgeNumber) ]); } RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)permissions) { + if (RCTRunningInAppExtension()) { + return; + } + UIUserNotificationType types = UIUserNotificationTypeNone; if (permissions) { if ([permissions[@"alert"] boolValue]) { @@ -152,35 +156,37 @@ RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)permissions) types = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound; } + UIApplication *app = RCTSharedApplication(); #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 - id notificationSettings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; - [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; - [[UIApplication sharedApplication] registerForRemoteNotifications]; - + [app registerUserNotificationSettings:notificationSettings]; + [app registerForRemoteNotifications]; #else - - [[UIApplication sharedApplication] registerForRemoteNotificationTypes:types]; - + [app registerForRemoteNotificationTypes:types]; #endif - } RCT_EXPORT_METHOD(abandonPermissions) { - [[UIApplication sharedApplication] unregisterForRemoteNotifications]; + [RCTSharedApplication() unregisterForRemoteNotifications]; } RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback) { + if (RCTRunningInAppExtension()) { + NSDictionary *permissions = @{@"alert": @(NO), @"badge": @(NO), @"sound": @(NO)}; + callback(@[permissions]); + return; + } + NSUInteger types = 0; if ([UIApplication instancesRespondToSelector:@selector(currentUserNotificationSettings)]) { - types = [[UIApplication sharedApplication] currentUserNotificationSettings].types; + types = [RCTSharedApplication() currentUserNotificationSettings].types; } else { #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 - types = [[UIApplication sharedApplication] enabledRemoteNotificationTypes]; + types = [RCTSharedApplication() enabledRemoteNotificationTypes]; #endif @@ -203,13 +209,13 @@ RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback) RCT_EXPORT_METHOD(presentLocalNotification:(UILocalNotification *)notification) { - [[UIApplication sharedApplication] presentLocalNotificationNow:notification]; + [RCTSharedApplication() presentLocalNotificationNow:notification]; } RCT_EXPORT_METHOD(scheduleLocalNotification:(UILocalNotification *)notification) { - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; + [RCTSharedApplication() scheduleLocalNotification:notification]; } @end diff --git a/React/Base/RCTPerfStats.m b/React/Base/RCTPerfStats.m index ba841dc67..462aa0f9e 100644 --- a/React/Base/RCTPerfStats.m +++ b/React/Base/RCTPerfStats.m @@ -10,6 +10,7 @@ #import "RCTPerfStats.h" #import "RCTDefines.h" +#import "RCTUtils.h" #if RCT_DEV @@ -66,7 +67,11 @@ RCT_EXPORT_MODULE() - (void)show { - UIView *targetView = [UIApplication sharedApplication].delegate.window.rootViewController.view; + if (RCTRunningInAppExtension()) { + return; + } + + UIView *targetView = RCTSharedApplication().delegate.window.rootViewController.view; targetView.frame = (CGRect){ targetView.frame.origin, diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 65eb99f8c..bc94d62f8 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -11,6 +11,7 @@ #import #import +#import #import "RCTAssert.h" #import "RCTDefines.h" @@ -51,6 +52,16 @@ RCT_EXTERN NSDictionary *RCTJSErrorFromNSError(NSError *error); // Returns YES if React is running in a test environment RCT_EXTERN BOOL RCTRunningInTestEnvironment(void); +// Returns YES if React is running in an iOS App Extension +RCT_EXTERN BOOL RCTRunningInAppExtension(void); + +// Returns the shared UIApplication instance, or nil if running in an App Extension +RCT_EXTERN UIApplication *RCTSharedApplication(void); + +// Return a UIAlertView initialized with the given values +// or nil if running in an app extension +RCT_EXTERN UIAlertView *RCTAlertView(NSString *title, NSString *message, id delegate, NSString *cancelButtonTitle, NSArray *otherButtonTitles); + // Return YES if image has an alpha component RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 2fba55f00..c3e59b06f 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -337,6 +337,42 @@ BOOL RCTRunningInTestEnvironment(void) return isTestEnvironment; } +BOOL RCTRunningInAppExtension(void) +{ + return [[[[NSBundle mainBundle] bundlePath] pathExtension] isEqualToString:@"appex"]; +} + +id RCTSharedApplication(void) +{ + if (RCTRunningInAppExtension()) { + return nil; + } + + return [[UIApplication class] performSelector:@selector(sharedApplication)]; +} + +id RCTAlertView(NSString *title, NSString *message, id delegate, NSString *cancelButtonTitle, NSArray *otherButtonTitles) +{ + if (RCTRunningInAppExtension()) { + RCTLogError(@"RCTAlertView is unavailable when running in an app extension"); + return nil; + } + + UIAlertView *alertView = [[UIAlertView alloc] init]; + alertView.title = title; + alertView.message = message; + alertView.delegate = delegate; + if (cancelButtonTitle != nil) { + [alertView addButtonWithTitle:cancelButtonTitle]; + alertView.cancelButtonIndex = 0; + } + for (NSString *buttonTitle in otherButtonTitles) + { + [alertView addButtonWithTitle:buttonTitle]; + } + return alertView; +} + BOOL RCTImageHasAlpha(CGImageRef image) { switch (CGImageGetAlphaInfo(image)) { diff --git a/React/Modules/RCTAlertManager.m b/React/Modules/RCTAlertManager.m index b0475e231..b07dcde54 100644 --- a/React/Modules/RCTAlertManager.m +++ b/React/Modules/RCTAlertManager.m @@ -11,6 +11,7 @@ #import "RCTAssert.h" #import "RCTLog.h" +#import "RCTUtils.h" @interface RCTAlertManager() @@ -76,13 +77,12 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args RCTLogError(@"Must have at least one button."); return; } - - UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title - message:nil - delegate:self - cancelButtonTitle:nil - otherButtonTitles:nil]; - + + if (RCTRunningInAppExtension()) { + return; + } + + UIAlertView *alertView = RCTAlertView(title, nil, self, nil, nil); NSMutableArray *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count]; if ([type isEqualToString:@"plain-text"]) { diff --git a/React/Modules/RCTAppState.m b/React/Modules/RCTAppState.m index a72ea2cc3..294330643 100644 --- a/React/Modules/RCTAppState.m +++ b/React/Modules/RCTAppState.m @@ -12,6 +12,7 @@ #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTEventDispatcher.h" +#import "RCTUtils.h" static NSString *RCTCurrentAppBackgroundState() { @@ -25,7 +26,11 @@ static NSString *RCTCurrentAppBackgroundState() }; }); - return states[@([UIApplication sharedApplication].applicationState)] ?: @"unknown"; + if (RCTRunningInAppExtension()) { + return @"extension"; + } + + return states[@(RCTSharedApplication().applicationState)] ?: @"unknown"; } @implementation RCTAppState diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index 63ac438d1..98335d7b4 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -407,11 +407,8 @@ RCT_EXPORT_MODULE() Class chromeExecutorClass = NSClassFromString(@"RCTWebSocketExecutor"); if (!chromeExecutorClass) { [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Chrome Debugger Unavailable" handler:^{ - [[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable" - message:@"You need to include the RCTWebSocket library to enable Chrome debugging" - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil] show]; + UIAlertView *alert = RCTAlertView(@"Chrome Debugger Unavailable", @"You need to include the RCTWebSocket library to enable Chrome debugging", nil, @"OK", nil); + [alert show]; }]]; } else { BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass; @@ -447,7 +444,7 @@ RCT_EXPORT_MODULE() RCT_EXPORT_METHOD(show) { - if (_actionSheet || !_bridge) { + if (_actionSheet || !_bridge || RCTRunningInAppExtension()) { return; } @@ -474,7 +471,7 @@ RCT_EXPORT_METHOD(show) actionSheet.cancelButtonIndex = actionSheet.numberOfButtons - 1; actionSheet.actionSheetStyle = UIBarStyleBlack; - [actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view]; + [actionSheet showInView:RCTSharedApplication().keyWindow.rootViewController.view]; _actionSheet = actionSheet; _presentedItems = items; } diff --git a/React/Modules/RCTRedBox.m b/React/Modules/RCTRedBox.m index 4d82278c9..ecf3fd921 100644 --- a/React/Modules/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -126,7 +126,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) { self.hidden = YES; [self resignFirstResponder]; - [[UIApplication sharedApplication].delegate.window makeKeyWindow]; + [RCTSharedApplication().delegate.window makeKeyWindow]; } - (void)reload diff --git a/React/Modules/RCTStatusBarManager.m b/React/Modules/RCTStatusBarManager.m index ce2364e2b..893c3e0f3 100644 --- a/React/Modules/RCTStatusBarManager.m +++ b/React/Modules/RCTStatusBarManager.m @@ -11,6 +11,7 @@ #import "RCTEventDispatcher.h" #import "RCTLog.h" +#import "RCTUtils.h" @implementation RCTConvert (UIStatusBar) @@ -97,8 +98,8 @@ RCT_EXPORT_METHOD(setStyle:(UIStatusBarStyle)statusBarStyle RCTLogError(@"RCTStatusBarManager module requires that the \ UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); } else { - [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle - animated:animated]; + [RCTSharedApplication() setStatusBarStyle:statusBarStyle + animated:animated]; } } @@ -109,14 +110,14 @@ RCT_EXPORT_METHOD(setHidden:(BOOL)hidden RCTLogError(@"RCTStatusBarManager module requires that the \ UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); } else { - [[UIApplication sharedApplication] setStatusBarHidden:hidden - withAnimation:animation]; + [RCTSharedApplication() setStatusBarHidden:hidden + withAnimation:animation]; } } RCT_EXPORT_METHOD(setNetworkActivityIndicatorVisible:(BOOL)visible) { - [UIApplication sharedApplication].networkActivityIndicatorVisible = visible; + RCTSharedApplication().networkActivityIndicatorVisible = visible; } @end