Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

App Extension support #1895

Closed
wants to merge 9 commits into from
16 changes: 13 additions & 3 deletions Libraries/ActionSheetIOS/RCTActionSheetManager.m
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
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
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
35 changes: 23 additions & 12 deletions Libraries/PushNotificationIOS/RCTPushNotificationManager.m
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,42 @@ - (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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you remove the whitespace here?

#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 +214,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
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
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
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)];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need the performSelector call here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the performSelector is necessary. If we try to invoke the method directly, the static analysis will catch us at compile time. We know it's safe to call, since we've done the runtime check, but Xcode will still yell at us.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering why it's necessary in this case but not in the case of UIAlertView for example. This code is probably fine, but ARC and performSelector don't always work well together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UIAlertView slips through because it's using init instead of the blacklisted initWithTitle:message:etc... selector. As far as I can tell, the NS_EXTENSION_UNAVAILABLE_IOS compiler flag just applies to method selectors, so we can work around flagged initializers by just calling init (or new), which is too generic to be flagged.

I think (but could be mistaken) that performSelector and ARC are only problematic if the selector isn't known at compile time (i.e. you're using NSSelectorFromString or something). Xcode is apparently smart enough to determine the return type and apply the correct ARC rules when you use @selector(selectorName), but not smart enough to apply the extension API blacklisting.

}

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
13 changes: 7 additions & 6 deletions React/Modules/RCTAlertManager.m
Expand Up @@ -11,6 +11,7 @@

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

@interface RCTAlertManager() <UIAlertViewDelegate>

Expand Down Expand Up @@ -69,12 +70,12 @@ - (dispatch_queue_t)methodQueue
RCTLogError(@"Must have at least one button.");
return;
}

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title
message:nil
delegate:self
cancelButtonTitle:nil
otherButtonTitles:nil];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: whitespace

if (RCTRunningInAppExtension()) {
return;
}

UIAlertView *alertView = RCTAlertView(title, nil, self, nil, nil);

NSMutableArray *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count];

Expand Down
7 changes: 6 additions & 1 deletion React/Modules/RCTAppState.m
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
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
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