From 0409d51fe96464e2b134145d6a021477cccfca19 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Fri, 6 Aug 2021 11:30:45 -0700 Subject: [PATCH 1/3] Fixes issue w/ UIApplication.shared not being supported in iOS App Extensions --- .../Platforms/iOS/iOSLifecycleMonitor.swift | 90 ++++++++++++------- Sources/Segment/Utilities/Utils.swift | 7 ++ 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift index 9e0dcaab..244d923d 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift @@ -11,34 +11,34 @@ import Foundation import UIKit public protocol iOSLifecycle { - func applicationDidEnterBackground(application: UIApplication) - func applicationWillEnterForeground(application: UIApplication) - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) - func applicationDidBecomeActive(application: UIApplication) - func applicationWillResignActive(application: UIApplication) - func applicationDidReceiveMemoryWarning(application: UIApplication) - func applicationWillTerminate(application: UIApplication) - func applicationSignificantTimeChange(application: UIApplication) - func applicationBackgroundRefreshDidChange(application: UIApplication, refreshStatus: UIBackgroundRefreshStatus) + func applicationDidEnterBackground(application: UIApplication?) + func applicationWillEnterForeground(application: UIApplication?) + func application(_ application: UIApplication?, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) + func applicationDidBecomeActive(application: UIApplication?) + func applicationWillResignActive(application: UIApplication?) + func applicationDidReceiveMemoryWarning(application: UIApplication?) + func applicationWillTerminate(application: UIApplication?) + func applicationSignificantTimeChange(application: UIApplication?) + func applicationBackgroundRefreshDidChange(application: UIApplication?, refreshStatus: UIBackgroundRefreshStatus) } public extension iOSLifecycle { - func applicationDidEnterBackground(application: UIApplication) { } - func applicationWillEnterForeground(application: UIApplication) { } - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { } - func applicationDidBecomeActive(application: UIApplication) { } - func applicationWillResignActive(application: UIApplication) { } - func applicationDidReceiveMemoryWarning(application: UIApplication) { } - func applicationWillTerminate(application: UIApplication) { } - func applicationSignificantTimeChange(application: UIApplication) { } - func applicationBackgroundRefreshDidChange(application: UIApplication, refreshStatus: UIBackgroundRefreshStatus) { } + func applicationDidEnterBackground(application: UIApplication?) { } + func applicationWillEnterForeground(application: UIApplication?) { } + func application(_ application: UIApplication?, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { } + func applicationDidBecomeActive(application: UIApplication?) { } + func applicationWillResignActive(application: UIApplication?) { } + func applicationDidReceiveMemoryWarning(application: UIApplication?) { } + func applicationWillTerminate(application: UIApplication?) { } + func applicationSignificantTimeChange(application: UIApplication?) { } + func applicationBackgroundRefreshDidChange(application: UIApplication?, refreshStatus: UIBackgroundRefreshStatus) { } } class iOSLifecycleMonitor: PlatformPlugin { let type = PluginType.utility var analytics: Analytics? - private var application: UIApplication + private var application: UIApplication? = nil private var appNotifications: [NSNotification.Name] = [UIApplication.didEnterBackgroundNotification, UIApplication.willEnterForegroundNotification, UIApplication.didFinishLaunchingNotification, @@ -50,7 +50,10 @@ class iOSLifecycleMonitor: PlatformPlugin { UIApplication.backgroundRefreshStatusDidChangeNotification] required init() { - application = UIApplication.shared + // App extensions can't use UIAppication.shared. + if !isAppExtension { + application = UIApplication.safeShared + } setupListeners() } @@ -154,10 +157,15 @@ class iOSLifecycleMonitor: PlatformPlugin { } func backgroundRefreshDidChange(notification: NSNotification) { - analytics?.apply { (ext) in - if let validExt = ext as? iOSLifecycle { - validExt.applicationBackgroundRefreshDidChange(application: application, - refreshStatus: application.backgroundRefreshStatus) + // Not only would we not get this in an App Extension, but it would + // be useless since we couldn't provide the application object or + // the refreshStatus value. + if !isAppExtension, let application = UIApplication.safeShared { + analytics?.apply { (ext) in + if let validExt = ext as? iOSLifecycle { + validExt.applicationBackgroundRefreshDidChange(application: application, + refreshStatus: application.backgroundRefreshStatus) + } } } } @@ -166,11 +174,11 @@ class iOSLifecycleMonitor: PlatformPlugin { // MARK: - Segment Destination Extension extension SegmentDestination: iOSLifecycle { - public func applicationWillEnterForeground(application: UIApplication) { + public func applicationWillEnterForeground(application: UIApplication?) { enterForeground() } - public func applicationDidEnterBackground(application: UIApplication) { + public func applicationDidEnterBackground(application: UIApplication?) { enterBackground() } } @@ -180,15 +188,31 @@ extension SegmentDestination.UploadTaskInfo { self.url = url self.task = task - let taskIdentifier = UIApplication.shared.beginBackgroundTask { [self] in - self.task.suspend() - self.cleanup?() + if let application = UIApplication.safeShared { + let taskIdentifier = application.beginBackgroundTask { [self] in + self.task.suspend() + self.cleanup?() + } + self.taskID = taskIdentifier.rawValue + + self.cleanup = { [self] in + application.endBackgroundTask(UIBackgroundTaskIdentifier(rawValue: self.taskID)) + } } - self.taskID = taskIdentifier.rawValue - - self.cleanup = { [self] in - UIApplication.shared.endBackgroundTask(UIBackgroundTaskIdentifier(rawValue: self.taskID)) + } +} + +extension UIApplication { + static var safeShared: UIApplication? { + // UIApplication.shared is not available in app extensions. + if !isAppExtension { + // getting it like this allows us to avoid the compiler error that would + // be generated even though we're guarding against app extensions. + // there's no preprocessor macro or @available macro to help us here unfortunately + // so this is the best i could do. + return UIApplication.value(forKeyPath: "sharedApplication") as? UIApplication } + return nil } } diff --git a/Sources/Segment/Utilities/Utils.swift b/Sources/Segment/Utilities/Utils.swift index 4d47742c..c44a91a1 100644 --- a/Sources/Segment/Utilities/Utils.swift +++ b/Sources/Segment/Utilities/Utils.swift @@ -13,6 +13,13 @@ internal var isUnitTesting: Bool = { return value }() +internal var isAppExtension: Bool = { + if Bundle.main.bundlePath.hasSuffix(".appex") { + return true + } + return false +}() + internal func exceptionFailure(_ message: String) { if isUnitTesting { assertionFailure(message) From a3c3c646220882e099c1a21f996cc4667a522021 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Fri, 6 Aug 2021 11:34:50 -0700 Subject: [PATCH 2/3] Removed unnecessary check. --- .../Plugins/Platforms/iOS/iOSLifecycleMonitor.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift index 244d923d..9f0aaba7 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift @@ -50,10 +50,9 @@ class iOSLifecycleMonitor: PlatformPlugin { UIApplication.backgroundRefreshStatusDidChangeNotification] required init() { - // App extensions can't use UIAppication.shared. - if !isAppExtension { - application = UIApplication.safeShared - } + // App extensions can't use UIAppication.shared, so + // funnel it through something to check; Could be nil. + application = UIApplication.safeShared setupListeners() } From 5549cd80c894da76b394b35548963c9084cb1b20 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Fri, 6 Aug 2021 12:02:05 -0700 Subject: [PATCH 3/3] Clarified commentary around safeShared. --- .../Plugins/Platforms/iOS/iOSLifecycleMonitor.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift index 9f0aaba7..f9406a4a 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift @@ -203,7 +203,10 @@ extension SegmentDestination.UploadTaskInfo { extension UIApplication { static var safeShared: UIApplication? { - // UIApplication.shared is not available in app extensions. + // UIApplication.shared is not available in app extensions so try to get + // it in a way that's safe for both. + + // if we are NOT an app extension, we need to get UIApplication.shared if !isAppExtension { // getting it like this allows us to avoid the compiler error that would // be generated even though we're guarding against app extensions. @@ -211,6 +214,8 @@ extension UIApplication { // so this is the best i could do. return UIApplication.value(forKeyPath: "sharedApplication") as? UIApplication } + // if we ARE an app extension, send back nil since we have no way to get the + // application instance. return nil } }