Files
Schnappix/node_modules/expo-modules-core/ios/AppDelegates/ExpoAppDelegateSubscriberManager.swift
T

513 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Dispatch
import Foundation
public class ExpoAppDelegateSubscriberManager: NSObject {
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
$0.responds(to: #selector(UIApplicationDelegate.application(_:willFinishLaunchingWithOptions:)))
}
// If we can't find a subscriber that implements `willFinishLaunchingWithOptions`, we will delegate the decision if we can handle the passed URL to
// the `didFinishLaunchingWithOptions` method by returning `true` here.
// You can read more about how iOS handles deep links here: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application#discussion
if parsedSubscribers.isEmpty {
return true
}
return parsedSubscribers.reduce(false) { result, subscriber in
return subscriber.application?(application, willFinishLaunchingWithOptions: launchOptions) ?? false || result
}
}
@objc
public static func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
ExpoAppDelegateSubscriberRepository.subscribers.forEach { subscriber in
// Subscriber result is ignored as it doesn't matter if any subscriber handled the incoming URL we always return `true` anyway.
_ = subscriber.application?(application, didFinishLaunchingWithOptions: launchOptions)
}
return true
}
#elseif os(macOS)
@objc
public static func applicationWillFinishLaunching(_ notification: Notification) {
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
$0.responds(to: #selector(NSApplicationDelegate.applicationWillFinishLaunching(_:)))
}
parsedSubscribers.forEach { subscriber in
subscriber.applicationWillFinishLaunching?(notification)
}
}
@objc
public static func applicationDidFinishLaunching(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { subscriber in
// Subscriber result is ignored as it doesn't matter if any subscriber handled the incoming URL we always return `true` anyway.
_ = subscriber.applicationDidFinishLaunching?(notification)
}
}
// TODO: - Configuring and Discarding Scenes
#endif
// MARK: - Responding to App Life-Cycle Events
#if os(iOS) || os(tvOS)
@objc
public static func applicationDidBecomeActive(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidBecomeActive?(application) }
}
@objc
public static func applicationWillResignActive(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillResignActive?(application) }
}
@objc
public static func applicationDidEnterBackground(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidEnterBackground?(application) }
}
@objc
public static func applicationWillEnterForeground(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillEnterForeground?(application) }
}
@objc
public static func applicationWillTerminate(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillTerminate?(application) }
}
#elseif os(macOS)
@objc
public static func applicationDidBecomeActive(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidBecomeActive?(notification) }
}
@objc
public static func applicationWillResignActive(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillResignActive?(notification) }
}
@objc
public static func applicationDidHide(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidHide?(notification) }
}
@objc
public static func applicationWillUnhide(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillUnhide?(notification) }
}
@objc
public static func applicationWillTerminate(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillTerminate?(notification) }
}
#endif
// MARK: - Responding to Environment Changes
#if os(iOS) || os(tvOS)
@objc
public static func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidReceiveMemoryWarning?(application) }
}
#endif
// TODO: - Managing App State Restoration
// MARK: - Downloading Data in the Background
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:handleEventsForBackgroundURLSession:completionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
let dispatchQueue = DispatchQueue(label: "expo.application.handleBackgroundEvents")
let handler = {
dispatchQueue.sync {
subscribersLeft -= 1
if subscribersLeft == 0 {
completionHandler()
}
}
}
if subs.isEmpty {
completionHandler()
} else {
subs.forEach {
$0.application?(application, handleEventsForBackgroundURLSession: identifier, completionHandler: handler)
}
}
}
#endif
// MARK: - Handling Remote Notification Registration
@objc
public static func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) }
}
@objc
public static func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.application?(application, didFailToRegisterForRemoteNotificationsWithError: error) }
}
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:didReceiveRemoteNotification:fetchCompletionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
let dispatchQueue = DispatchQueue(label: "expo.application.remoteNotification", qos: .userInteractive)
var failedCount = 0
var newDataCount = 0
let handler = { (result: UIBackgroundFetchResult) in
dispatchQueue.sync {
if result == .failed {
failedCount += 1
} else if result == .newData {
newDataCount += 1
}
subscribersLeft -= 1
if subscribersLeft == 0 {
if newDataCount > 0 {
completionHandler(.newData)
} else if failedCount > 0 {
completionHandler(.failed)
} else {
completionHandler(.noData)
}
}
}
}
if subs.isEmpty {
completionHandler(.noData)
} else {
subs.forEach { subscriber in
subscriber.application?(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: handler)
}
}
}
#elseif os(macOS)
@objc
public static func application(
_ application: NSApplication,
didReceiveRemoteNotification userInfo: [String: Any]
) {
let selector = #selector(NSApplicationDelegate.application(_:didReceiveRemoteNotification:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
subs.forEach { subscriber in
subscriber.application?(application, didReceiveRemoteNotification: userInfo)
}
}
#endif
// MARK: - Continuing User Activity and Handling Quick Actions
@objc
public static func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
return ExpoAppDelegateSubscriberRepository
.subscribers
.reduce(false) { result, subscriber in
return subscriber.application?(application, willContinueUserActivityWithType: userActivityType) ?? false || result
}
}
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let selector = #selector(UIApplicationDelegate.application(_:continue:restorationHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
let dispatchQueue = DispatchQueue(label: "expo.application.continueUserActivity", qos: .userInteractive)
var allRestorableObjects = [UIUserActivityRestoring]()
let handler = { (restorableObjects: [UIUserActivityRestoring]?) in
dispatchQueue.sync {
if let restorableObjects = restorableObjects {
allRestorableObjects.append(contentsOf: restorableObjects)
}
subscribersLeft -= 1
if subscribersLeft == 0 {
restorationHandler(allRestorableObjects)
}
}
}
return subs.reduce(false) { result, subscriber in
return subscriber.application?(application, continue: userActivity, restorationHandler: handler) ?? false || result
}
}
#elseif os(macOS)
@objc
public static func application(
_ application: NSApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void
) -> Bool {
let selector = #selector(NSApplicationDelegate.application(_:continue:restorationHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
let dispatchQueue = DispatchQueue(label: "expo.application.continueUserActivity", qos: .userInteractive)
var allRestorableObjects = [NSUserActivityRestoring]()
let handler = { (restorableObjects: [NSUserActivityRestoring]?) in
dispatchQueue.sync {
if let restorableObjects = restorableObjects {
allRestorableObjects.append(contentsOf: restorableObjects)
}
subscribersLeft -= 1
if subscribersLeft == 0 {
restorationHandler(allRestorableObjects)
}
}
}
return subs.reduce(false) { result, subscriber in
return subscriber.application?(application, continue: userActivity, restorationHandler: handler) ?? false || result
}
}
#endif
@objc
public static func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
return ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.application?(application, didUpdate: userActivity) }
}
@objc
public static func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
return ExpoAppDelegateSubscriberRepository
.subscribers
.forEach {
$0.application?(application, didFailToContinueUserActivityWithType: userActivityType, error: error)
}
}
#if os(iOS)
@objc
public static func application(
_ application: UIApplication,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:performActionFor:completionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
var result: Bool = false
let dispatchQueue = DispatchQueue(label: "expo.application.performAction", qos: .userInteractive)
let handler = { (succeeded: Bool) in
dispatchQueue.sync {
result = result || succeeded
subscribersLeft -= 1
if subscribersLeft == 0 {
completionHandler(result)
}
}
}
if subs.isEmpty {
completionHandler(result)
} else {
subs.forEach { subscriber in
subscriber.application?(application, performActionFor: shortcutItem, completionHandler: handler)
}
}
}
#endif
// MARK: - Background Fetch
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:performFetchWithCompletionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
let dispatchQueue = DispatchQueue(label: "expo.application.performFetch", qos: .userInteractive)
var failedCount = 0
var newDataCount = 0
let handler = { (result: UIBackgroundFetchResult) in
dispatchQueue.sync {
if result == .failed {
failedCount += 1
} else if result == .newData {
newDataCount += 1
}
subscribersLeft -= 1
if subscribersLeft == 0 {
if newDataCount > 0 {
completionHandler(.newData)
} else if failedCount > 0 {
completionHandler(.failed)
} else {
completionHandler(.noData)
}
}
}
}
if subs.isEmpty {
completionHandler(.noData)
} else {
subs.forEach { subscriber in
subscriber.application?(application, performFetchWithCompletionHandler: handler)
}
}
}
#endif
// MARK: - Opening a URL-Specified Resource
#if os(iOS) || os(tvOS)
@objc
public static func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return ExpoAppDelegateSubscriberRepository.subscribers.reduce(false) { result, subscriber in
return subscriber.application?(app, open: url, options: options) ?? false || result
}
}
#elseif os(macOS)
@objc
public static func application(_ app: NSApplication, open urls: [URL]) {
ExpoAppDelegateSubscriberRepository.subscribers.forEach { subscriber in
subscriber.application?(app, open: urls)
}
}
#endif
#if os(iOS)
/**
* Sets allowed orientations for the application. It will use the values from `Info.plist`as the orientation mask unless a subscriber requested
* a different orientation.
*/
@objc
public static func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
let deviceOrientationMask = allowedOrientations(for: UIDevice.current.userInterfaceIdiom)
let universalOrientationMask = allowedOrientations(for: .unspecified)
let infoPlistOrientations = deviceOrientationMask.isEmpty ? universalOrientationMask : deviceOrientationMask
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
$0.responds(to: #selector(UIApplicationDelegate.application(_:supportedInterfaceOrientationsFor:)))
}
// We want to create an intersection of all orientations set by subscribers.
let subscribersMask: UIInterfaceOrientationMask = parsedSubscribers.reduce(.all) { result, subscriber in
guard let requestedOrientation = subscriber.application?(application, supportedInterfaceOrientationsFor: window) else {
return result
}
return requestedOrientation.intersection(result)
}
return parsedSubscribers.isEmpty ? infoPlistOrientations : subscribersMask
}
#endif
}
#if os(iOS)
private func allowedOrientations(for userInterfaceIdiom: UIUserInterfaceIdiom) -> UIInterfaceOrientationMask {
// For now only iPad-specific orientations are supported
let deviceString = userInterfaceIdiom == .pad ? "~pad" : ""
var mask: UIInterfaceOrientationMask = []
guard let orientations = Bundle.main.infoDictionary?["UISupportedInterfaceOrientations\(deviceString)"] as? [String] else {
return mask
}
for orientation in orientations {
switch orientation {
case "UIInterfaceOrientationPortrait":
mask.insert(.portrait)
case "UIInterfaceOrientationLandscapeLeft":
mask.insert(.landscapeLeft)
case "UIInterfaceOrientationLandscapeRight":
mask.insert(.landscapeRight)
case "UIInterfaceOrientationPortraitUpsideDown":
mask.insert(.portraitUpsideDown)
default:
break
}
}
return mask
}
#endif // os(iOS)