Skip to content

Commit

Permalink
App Extension support
Browse files Browse the repository at this point in the history
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 facebook#1895

Reviewed By: @​svcscm

Differential Revision: D2224128

Pulled By: @nicklockwood
  • Loading branch information
yusefnapora authored and facebook-github-bot-5 committed Sep 22, 2015
1 parent 3f220f6 commit 2f9bd1f
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 46 deletions.
16 changes: 13 additions & 3 deletions Libraries/ActionSheetIOS/RCTActionSheetManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ - (dispatch_queue_t)methodQueue
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"];
Expand All @@ -62,7 +67,7 @@ - (dispatch_queue_t)methodQueue

_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;
Expand All @@ -87,8 +92,13 @@ - (dispatch_queue_t)methodQueue
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

Expand Down Expand Up @@ -146,7 +156,7 @@ - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger
RCTLogWarn(@"No callback registered for action sheet: %@", actionSheet.title);
}

[[UIApplication sharedApplication].delegate.window makeKeyWindow];
[RCTSharedApplication().delegate.window makeKeyWindow];
}

#pragma mark Private
Expand Down
20 changes: 16 additions & 4 deletions Libraries/CameraRoll/RCTImagePickerManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

#import "RCTImagePickerManager.h"
#import "RCTRootView.h"
#import "RCTLog.h"
#import "RCTUtils.h"

#import <UIKit/UIKit.h>

Expand Down Expand Up @@ -53,7 +55,12 @@ - (instancetype)init
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];
Expand All @@ -75,7 +82,12 @@ - (instancetype)init
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];
Expand Down Expand Up @@ -109,7 +121,7 @@ - (void)imagePickerController:(UIImagePickerController *)picker
[_pickerCallbacks removeObjectAtIndex:index];
[_pickerCancelCallbacks removeObjectAtIndex:index];

UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
UIWindow *keyWindow = RCTSharedApplication().keyWindow;
UIViewController *rootViewController = keyWindow.rootViewController;
[rootViewController dismissViewControllerAnimated:YES completion:nil];

Expand All @@ -125,7 +137,7 @@ - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
[_pickerCallbacks removeObjectAtIndex:index];
[_pickerCancelCallbacks removeObjectAtIndex:index];

UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
UIWindow *keyWindow = RCTSharedApplication().keyWindow;
UIViewController *rootViewController = keyWindow.rootViewController;
[rootViewController dismissViewControllerAnimated:YES completion:nil];

Expand Down
10 changes: 8 additions & 2 deletions Libraries/LinkingIOS/RCTLinkingManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,20 @@ - (void)handleOpenURLNotification:(NSNotification *)notification
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)]);
}

Expand Down
36 changes: 21 additions & 15 deletions Libraries/PushNotificationIOS/RCTPushNotificationManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification
*/
RCT_EXPORT_METHOD(setApplicationIconBadgeNumber:(NSInteger)number)
{
[UIApplication sharedApplication].applicationIconBadgeNumber = number;
RCTSharedApplication().applicationIconBadgeNumber = number;
}

/**
Expand All @@ -131,12 +131,16 @@ - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification
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]) {
Expand All @@ -152,35 +156,37 @@ - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification
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

Expand All @@ -203,13 +209,13 @@ - (NSDictionary *)constantsToExport

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
7 changes: 6 additions & 1 deletion React/Base/RCTPerfStats.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import "RCTPerfStats.h"

#import "RCTDefines.h"
#import "RCTUtils.h"

#if RCT_DEV

Expand Down Expand Up @@ -66,7 +67,11 @@ - (RCTFPSGraph *)uiGraph

- (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,
Expand Down
11 changes: 11 additions & 0 deletions React/Base/RCTUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#import <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#import "RCTAssert.h"
#import "RCTDefines.h"
Expand Down Expand Up @@ -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);

Expand Down
36 changes: 36 additions & 0 deletions React/Base/RCTUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
14 changes: 7 additions & 7 deletions React/Modules/RCTAlertManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#import "RCTAssert.h"
#import "RCTLog.h"
#import "RCTUtils.h"

@interface RCTAlertManager() <UIAlertViewDelegate>

Expand Down Expand Up @@ -76,13 +77,12 @@ - (void)invalidate
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"]) {
Expand Down
7 changes: 6 additions & 1 deletion React/Modules/RCTAppState.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
#import "RCTUtils.h"

static NSString *RCTCurrentAppBackgroundState()
{
Expand All @@ -25,7 +26,11 @@
};
});

return states[@([UIApplication sharedApplication].applicationState)] ?: @"unknown";
if (RCTRunningInAppExtension()) {
return @"extension";
}

return states[@(RCTSharedApplication().applicationState)] ?: @"unknown";
}

@implementation RCTAppState
Expand Down
11 changes: 4 additions & 7 deletions React/Modules/RCTDevMenu.m
Original file line number Diff line number Diff line change
Expand Up @@ -407,11 +407,8 @@ - (NSArray *)menuItems
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;
Expand Down Expand Up @@ -447,7 +444,7 @@ - (NSArray *)menuItems

RCT_EXPORT_METHOD(show)
{
if (_actionSheet || !_bridge) {
if (_actionSheet || !_bridge || RCTRunningInAppExtension()) {
return;
}

Expand All @@ -474,7 +471,7 @@ - (NSArray *)menuItems
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;
}
Expand Down
2 changes: 1 addition & 1 deletion React/Modules/RCTRedBox.m
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ - (void)dismiss
{
self.hidden = YES;
[self resignFirstResponder];
[[UIApplication sharedApplication].delegate.window makeKeyWindow];
[RCTSharedApplication().delegate.window makeKeyWindow];
}

- (void)reload
Expand Down
Loading

0 comments on commit 2f9bd1f

Please sign in to comment.