Created
October 30, 2018 21:22
-
-
Save lnfnunes/6be40fb8d0a3fe9d973330c48abf2998 to your computer and use it in GitHub Desktop.
Expo SDK 26 compatibility with Xcode 10
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2015-present 650 Industries. All rights reserved. | |
#import "EXAnalytics.h" | |
#import "EXAppState.h" | |
#import "EXAppViewController.h" | |
#import "EXBuildConstants.h" | |
#import "EXKernel.h" | |
#import "EXKernelAppLoader.h" | |
#import "EXKernelAppRecord.h" | |
#import "EXKernelLinkingManager.h" | |
#import "EXLinkingManager.h" | |
#import "EXVersions.h" | |
#import <React/RCTBridge+Private.h> | |
#import <React/RCTEventDispatcher.h> | |
#import <React/RCTModuleData.h> | |
#import <React/RCTUtils.h> | |
NS_ASSUME_NONNULL_BEGIN | |
NSString *kEXKernelErrorDomain = @"EXKernelErrorDomain"; | |
NSString *kEXKernelShouldForegroundTaskEvent = @"foregroundTask"; | |
NSString * const kEXDeviceInstallUUIDKey = @"EXDeviceInstallUUIDKey"; | |
NSString * const kEXKernelClearJSCacheUserDefaultsKey = @"EXKernelClearJSCacheUserDefaultsKey"; | |
@interface EXKernel () <EXKernelAppRegistryDelegate> | |
@end | |
// Protocol that should be implemented by all versions of EXAppState class. | |
@protocol EXAppStateProtocol | |
@property (nonatomic, strong, readonly) NSString *lastKnownState; | |
- (void)setState:(NSString *)state; | |
@end | |
@implementation EXKernel | |
+ (instancetype)sharedInstance | |
{ | |
static EXKernel *theKernel; | |
static dispatch_once_t once; | |
dispatch_once(&once, ^{ | |
if (!theKernel) { | |
theKernel = [[EXKernel alloc] init]; | |
} | |
}); | |
return theKernel; | |
} | |
- (instancetype)init | |
{ | |
if (self = [super init]) { | |
// init app registry: keep track of RN bridges we are running | |
_appRegistry = [[EXKernelAppRegistry alloc] init]; | |
_appRegistry.delegate = self; | |
// init service registry: classes which manage shared resources among all bridges | |
_serviceRegistry = [[EXKernelServiceRegistry alloc] init]; | |
for (NSString *name in @[UIApplicationDidBecomeActiveNotification, | |
UIApplicationDidEnterBackgroundNotification, | |
UIApplicationDidFinishLaunchingNotification, | |
UIApplicationWillResignActiveNotification, | |
UIApplicationWillEnterForegroundNotification]) { | |
[[NSNotificationCenter defaultCenter] addObserver:self | |
selector:@selector(_handleAppStateDidChange:) | |
name:name | |
object:nil]; | |
} | |
NSLog(@"Expo iOS Runtime Version %@", [EXBuildConstants sharedInstance].expoRuntimeVersion); | |
} | |
return self; | |
} | |
- (void)dealloc | |
{ | |
[[NSNotificationCenter defaultCenter] removeObserver:self]; | |
} | |
#pragma mark - Misc | |
+ (NSString *)deviceInstallUUID | |
{ | |
NSString *uuid = [[NSUserDefaults standardUserDefaults] stringForKey:kEXDeviceInstallUUIDKey]; | |
if (!uuid) { | |
uuid = [[NSUUID UUID] UUIDString]; | |
[[NSUserDefaults standardUserDefaults] setObject:uuid forKey:kEXDeviceInstallUUIDKey]; | |
[[NSUserDefaults standardUserDefaults] synchronize]; | |
} | |
return uuid; | |
} | |
- (void)logAnalyticsEvent:(NSString *)eventId forAppRecord:(EXKernelAppRecord *)appRecord | |
{ | |
if (_appRegistry.homeAppRecord && appRecord == _appRegistry.homeAppRecord) { | |
return; | |
} | |
NSString *validatedSdkVersion = [[EXVersions sharedInstance] availableSdkVersionForManifest:appRecord.appLoader.manifest]; | |
NSDictionary *props = (validatedSdkVersion) ? @{ @"SDK_VERSION": validatedSdkVersion } : @{}; | |
[[EXAnalytics sharedInstance] logEvent:eventId | |
manifestUrl:appRecord.appLoader.manifestUrl | |
eventProperties:props]; | |
} | |
#pragma mark - bridge registry delegate | |
- (void)appRegistry:(EXKernelAppRegistry *)registry didRegisterAppRecord:(EXKernelAppRecord *)appRecord | |
{ | |
// forward to service registry | |
[_serviceRegistry appRegistry:registry didRegisterAppRecord:appRecord]; | |
} | |
- (void)appRegistry:(EXKernelAppRegistry *)registry willUnregisterAppRecord:(EXKernelAppRecord *)appRecord | |
{ | |
// forward to service registry | |
[_serviceRegistry appRegistry:registry willUnregisterAppRecord:appRecord]; | |
} | |
#pragma mark - Interfacing with JS | |
- (void)sendUrl:(NSString *)urlString toAppRecord:(EXKernelAppRecord *)app | |
{ | |
// fire a Linking url event on this (possibly versioned) bridge | |
EXReactAppManager *appManager = app.appManager; | |
id linkingModule = [self nativeModuleForAppManager:appManager named:@"LinkingManager"]; | |
if (!linkingModule) { | |
DDLogError(@"Could not find the Linking module to open URL (%@)", urlString); | |
} else if ([linkingModule respondsToSelector:@selector(dispatchOpenUrlEvent:)]) { | |
[linkingModule dispatchOpenUrlEvent:[NSURL URLWithString:urlString]]; | |
} else { | |
DDLogError(@"Linking module doesn't support the API we use to open URL (%@)", urlString); | |
} | |
[self _moveAppToVisible:app]; | |
} | |
- (id)nativeModuleForAppManager:(EXReactAppManager *)appManager named:(NSString *)moduleName | |
{ | |
id destinationBridge = appManager.reactBridge; | |
if ([destinationBridge respondsToSelector:@selector(batchedBridge)]) { | |
id batchedBridge = [destinationBridge batchedBridge]; | |
id moduleData = [batchedBridge moduleDataForName:moduleName]; | |
// React Native before SDK 11 didn't strip the "RCT" prefix from module names | |
if (!moduleData && ![moduleName hasPrefix:@"RCT"]) { | |
moduleData = [batchedBridge moduleDataForName:[@"RCT" stringByAppendingString:moduleName]]; | |
} | |
if (moduleData) { | |
return [moduleData instance]; | |
} | |
} else { | |
// bridge can be null if the record is in an error state and never created a bridge. | |
if (destinationBridge) { | |
DDLogError(@"Bridge does not support the API we use to get its underlying batched bridge"); | |
} | |
} | |
return nil; | |
} | |
- (void)sendNotification:(NSDictionary *)notifBody | |
toExperienceWithId:(NSString *)destinationExperienceId | |
fromBackground:(BOOL)isFromBackground | |
isRemote:(BOOL)isRemote | |
{ | |
EXKernelAppRecord *destinationApp = [_appRegistry newestRecordWithExperienceId:destinationExperienceId]; | |
// if the notification came from the background, in most but not all cases, this means the user acted on an iOS notification | |
// and caused the app to launch. | |
// From SO: | |
// > Note that "App opened from Notification" will be a false positive if the notification is sent while the user is on a different | |
// > screen (for example, if they pull down the status bar and then receive a notification from your app). | |
NSDictionary *bodyWithOrigin = @{ | |
@"origin": (isFromBackground) ? @"selected" : @"received", | |
@"remote": @(isRemote), | |
@"data": notifBody, | |
}; | |
if (destinationApp) { | |
// send the body to the already-open experience | |
[self _dispatchJSEvent:@"Exponent.notification" body:bodyWithOrigin toApp:destinationApp]; | |
[self _moveAppToVisible:destinationApp]; | |
} else { | |
// no app is currently running for this experience id. | |
// if we're Expo Client, we can query Home for a past experience in the user's history, and route the notification there. | |
if (_browserController) { | |
__weak typeof(self) weakSelf = self; | |
[_browserController getHistoryUrlForExperienceId:destinationExperienceId completion:^(NSString *urlString) { | |
if (urlString) { | |
NSURL *url = [NSURL URLWithString:urlString]; | |
if (url) { | |
[weakSelf createNewAppWithUrl:url initialProps:@{ @"notification": bodyWithOrigin }]; | |
} | |
} | |
}]; | |
} | |
} | |
} | |
/** | |
* If the bridge has a batchedBridge or parentBridge selector, posts the notification on that object as well. | |
*/ | |
- (void)_postNotificationName: (NSNotificationName)name onAbstractBridge: (id)bridge | |
{ | |
[[NSNotificationCenter defaultCenter] postNotificationName:name object:bridge]; | |
if ([bridge respondsToSelector:@selector(batchedBridge)]) { | |
[[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge batchedBridge]]; | |
} else if ([bridge respondsToSelector:@selector(parentBridge)]) { | |
[[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge parentBridge]]; | |
} | |
} | |
- (void)_dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody toApp:(EXKernelAppRecord *)appRecord | |
{ | |
[appRecord.appManager.reactBridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" | |
args:eventBody ? @[eventName, eventBody] : @[eventName]]; | |
} | |
#pragma mark - App State | |
- (EXKernelAppRecord *)createNewAppWithUrl:(NSURL *)url initialProps:(nullable NSDictionary *)initialProps | |
{ | |
NSString *recordId = [_appRegistry registerAppWithManifestUrl:url initialProps:initialProps]; | |
EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; | |
[self _moveAppToVisible:record]; | |
return record; | |
} | |
- (void)switchTasks | |
{ | |
if (!_browserController) { | |
return; | |
} | |
if (_visibleApp != _appRegistry.homeAppRecord) { | |
[EXUtil performSynchronouslyOnMainThread:^{ | |
[_browserController toggleMenuWithCompletion:nil]; | |
}]; | |
} else { | |
EXKernelAppRegistry *appRegistry = [EXKernel sharedInstance].appRegistry; | |
for (NSString *recordId in appRegistry.appEnumerator) { | |
EXKernelAppRecord *record = [appRegistry recordForId:recordId]; | |
// foreground the first thing we find | |
[self _moveAppToVisible:record]; | |
} | |
} | |
} | |
- (void)reloadAppWithExperienceId:(NSString *)experienceId | |
{ | |
EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithExperienceId:experienceId]; | |
if (_browserController) { | |
[self createNewAppWithUrl:appRecord.appLoader.manifestUrl initialProps:nil]; | |
} else if (_appRegistry.standaloneAppRecord && appRecord == _appRegistry.standaloneAppRecord) { | |
[appRecord.viewController refresh]; | |
} | |
} | |
- (void)viewController:(__unused EXViewController *)vc didNavigateAppToVisible:(EXKernelAppRecord *)appRecord | |
{ | |
EXKernelAppRecord *appRecordPreviouslyVisible = _visibleApp; | |
if (appRecord != appRecordPreviouslyVisible) { | |
if (appRecordPreviouslyVisible) { | |
[appRecordPreviouslyVisible.viewController appStateDidBecomeInactive]; | |
[self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appRecordPreviouslyVisible.appManager.reactBridge]; | |
id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecordPreviouslyVisible.appManager named:@"AppState"]; | |
if (appStateModule != nil) { | |
[appStateModule setState:@"background"]; | |
} | |
} | |
if (appRecord) { | |
[appRecord.viewController appStateDidBecomeActive]; | |
[self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appRecord.appManager.reactBridge]; | |
id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecord.appManager named:@"AppState"]; | |
if (appStateModule != nil) { | |
[appStateModule setState:@"active"]; | |
} | |
_visibleApp = appRecord; | |
[[EXAnalytics sharedInstance] logAppVisibleEvent]; | |
} else { | |
_visibleApp = nil; | |
} | |
if (_visibleApp && _visibleApp != _appRegistry.homeAppRecord) { | |
[self _unregisterUnusedAppRecords]; | |
} | |
} | |
} | |
- (void)_unregisterUnusedAppRecords | |
{ | |
for (NSString *recordId in _appRegistry.appEnumerator) { | |
EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; | |
if (record && record != _visibleApp) { | |
[_appRegistry unregisterAppWithRecordId:recordId]; | |
break; | |
} | |
} | |
} | |
- (void)_handleAppStateDidChange:(NSNotification *)notification | |
{ | |
NSString *newState; | |
if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) { | |
newState = @"inactive"; | |
} else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) { | |
newState = @"background"; | |
} else { | |
switch (RCTSharedApplication().applicationState) { | |
case UIApplicationStateActive: | |
newState = @"active"; | |
break; | |
case UIApplicationStateBackground: { | |
newState = @"background"; | |
break; | |
} | |
default: { | |
newState = @"unknown"; | |
break; | |
} | |
} | |
} | |
if (_visibleApp) { | |
EXReactAppManager *appManager = _visibleApp.appManager; | |
id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appManager named:@"AppState"]; | |
NSString *lastKnownState; | |
if (appStateModule != nil) { | |
lastKnownState = [appStateModule lastKnownState]; | |
[appStateModule setState:newState]; | |
} | |
if (!lastKnownState || ![newState isEqualToString:lastKnownState]) { | |
if ([newState isEqualToString:@"active"]) { | |
[_visibleApp.viewController appStateDidBecomeActive]; | |
[self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appManager.reactBridge]; | |
} else if ([newState isEqualToString:@"background"]) { | |
[_visibleApp.viewController appStateDidBecomeInactive]; | |
[self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appManager.reactBridge]; | |
} | |
} | |
} | |
} | |
- (void)_moveAppToVisible:(EXKernelAppRecord *)appRecord | |
{ | |
if (_browserController) { | |
[EXUtil performSynchronouslyOnMainThread:^{ | |
[_browserController moveAppToVisible:appRecord]; | |
}]; | |
} | |
} | |
@end | |
NS_ASSUME_NONNULL_END |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment