Skip to content

Fixes issue w/ UIApplication.shared not being supported in iOS App Extensions #37

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

Merged
merged 3 commits into from
Aug 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 60 additions & 32 deletions Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -50,7 +50,9 @@ class iOSLifecycleMonitor: PlatformPlugin {
UIApplication.backgroundRefreshStatusDidChangeNotification]

required init() {
application = UIApplication.shared
// App extensions can't use UIAppication.shared, so
// funnel it through something to check; Could be nil.
application = UIApplication.safeShared
setupListeners()
}

Expand Down Expand Up @@ -154,10 +156,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)
}
}
}
}
Expand All @@ -166,11 +173,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()
}
}
Expand All @@ -180,15 +187,36 @@ 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
}
}

extension UIApplication {
static var safeShared: UIApplication? {
// UIApplication.shared is not available in app extensions so try to get
// it in a way that's safe for both.

self.cleanup = { [self] in
UIApplication.shared.endBackgroundTask(UIBackgroundTaskIdentifier(rawValue: self.taskID))
// 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.
// 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
}
// if we ARE an app extension, send back nil since we have no way to get the
// application instance.
return nil
}
}

Expand Down
7 changes: 7 additions & 0 deletions Sources/Segment/Utilities/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down