App Extension support

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
This commit is contained in:
Yusef Napora 2015-09-22 10:43:56 -07:00 committed by facebook-github-bot-5
parent 3f220f6b59
commit 2f9bd1f62f
12 changed files with 135 additions and 46 deletions

View File

@ -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

View File

@ -10,6 +10,8 @@
#import "RCTImagePickerManager.h"
#import "RCTRootView.h"
#import "RCTLog.h"
#import "RCTUtils.h"
#import <UIKit/UIKit.h>
@ -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];

View File

@ -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)]);
}

View File

@ -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

View File

@ -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,

View File

@ -11,6 +11,7 @@
#import <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#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);

View File

@ -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)) {

View File

@ -11,6 +11,7 @@
#import "RCTAssert.h"
#import "RCTLog.h"
#import "RCTUtils.h"
@interface RCTAlertManager() <UIAlertViewDelegate>
@ -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"]) {

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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