diff --git a/Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift b/Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift index 3b82445..ced89eb 100644 --- a/Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift +++ b/Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift @@ -11,28 +11,42 @@ import Foundation import SwiftUI class IOSLifecycleMonitor: UtilityPlugin { - private var application: UIApplication? - private var appNotifications: [NSNotification.Name] = [ - UIApplication.didEnterBackgroundNotification, - UIApplication.willEnterForegroundNotification, - UIApplication.didFinishLaunchingNotification, - UIApplication.didBecomeActiveNotification, - ] + private var utils: DefaultEventUtils? private var sendApplicationOpenedOnDidBecomeActive = false override init() { - // TODO: Check if lifecycle plugin works for app extension - // App extensions can't use UIApplication.shared, so - // funnel it through something to check; Could be nil. - application = IOSVendorSystem.sharedApplication super.init() - setupListeners() + + NotificationCenter.default.addObserver(self, + selector: #selector(applicationDidFinishLaunchingNotification(notification:)), + name: UIApplication.didFinishLaunchingNotification, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(applicationDidBecomeActive(notification:)), + name: UIApplication.didBecomeActiveNotification, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(applicationWillEnterForeground(notification:)), + name: UIApplication.willEnterForegroundNotification, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(applicationDidEnterBackground(notification:)), + name: UIApplication.didEnterBackgroundNotification, + object: nil) } public override func setup(amplitude: Amplitude) { super.setup(amplitude: amplitude) utils = DefaultEventUtils(amplitude: amplitude) + + // If we are already in the foreground, dispatch installed / opened events now + if IOSVendorSystem.sharedApplication?.applicationState == .active { + utils?.trackAppUpdatedInstalledEvent() + amplitude.onEnterForeground(timestamp: currentTimestamp) + utils?.trackAppOpenedEvent() + } + if amplitude.configuration.autocapture.contains(.screenViews) { UIKitScreenViews.register(amplitude) } @@ -42,40 +56,39 @@ class IOSLifecycleMonitor: UtilityPlugin { } @objc - func notificationResponse(notification: Notification) { - switch notification.name { - case UIApplication.didEnterBackgroundNotification: - didEnterBackground(notification: notification) - case UIApplication.willEnterForegroundNotification: - applicationWillEnterForeground(notification: notification) - case UIApplication.didFinishLaunchingNotification: - applicationDidFinishLaunchingNotification(notification: notification) - case UIApplication.didBecomeActiveNotification: - applicationDidBecomeActive(notification: notification) - default: - break + func applicationDidFinishLaunchingNotification(notification: Notification) { + utils?.trackAppUpdatedInstalledEvent() + + // Pre SceneDelegate apps wil not fire a willEnterForeground notification on app launch. + // Instead, use the initial applicationDidBecomeActive + let sceneManifest = Bundle.main.infoDictionary?["UIApplicationSceneManifest"] as? [String: Any] + let sceneConfigurations = sceneManifest?["UISceneConfigurations"] as? [String: Any] ?? [:] + let hasSceneConfigurations = !sceneConfigurations.isEmpty + + let appDelegate = IOSVendorSystem.sharedApplication?.delegate + let selector = #selector(UIApplicationDelegate.application(_:configurationForConnecting:options:)) + let usesSceneDelegate = appDelegate?.responds(to: selector) ?? false + + if !(hasSceneConfigurations || usesSceneDelegate) { + sendApplicationOpenedOnDidBecomeActive = true } } - func setupListeners() { - // Configure the current life cycle events - let notificationCenter = NotificationCenter.default - for notification in appNotifications { - notificationCenter.addObserver( - self, - selector: #selector(notificationResponse(notification:)), - name: notification, - object: application - ) + @objc + func applicationDidBecomeActive(notification: Notification) { + guard sendApplicationOpenedOnDidBecomeActive else { + return } + sendApplicationOpenedOnDidBecomeActive = false + amplitude?.onEnterForeground(timestamp: currentTimestamp) + utils?.trackAppOpenedEvent() } + @objc func applicationWillEnterForeground(notification: Notification) { - let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000) - let fromBackground: Bool - if let sharedApplication = application { + if let sharedApplication = IOSVendorSystem.sharedApplication { switch sharedApplication.applicationState { case .active, .inactive: fromBackground = false @@ -88,52 +101,23 @@ class IOSLifecycleMonitor: UtilityPlugin { fromBackground = false } - amplitude?.onEnterForeground(timestamp: timestamp) - sendApplicationOpened(fromBackground: fromBackground) + amplitude?.onEnterForeground(timestamp: currentTimestamp) + utils?.trackAppOpenedEvent(fromBackground: fromBackground) } - func didEnterBackground(notification: Notification) { - let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000) - self.amplitude?.onExitForeground(timestamp: timestamp) - if amplitude?.configuration.autocapture.contains(.appLifecycles) ?? false { - self.amplitude?.track(eventType: Constants.AMP_APPLICATION_BACKGROUNDED_EVENT) - } - } - - func applicationDidFinishLaunchingNotification(notification: Notification) { - utils?.trackAppUpdatedInstalledEvent() - - // Pre SceneDelegate apps wil not fire a willEnterForeground notification on app launch. - // Instead, use the initial applicationDidBecomeActive - let usesSceneDelegate = application?.delegate?.responds(to: #selector(UIApplicationDelegate.application(_:configurationForConnecting:options:))) ?? false - if !usesSceneDelegate { - sendApplicationOpenedOnDidBecomeActive = true - } - } - - func applicationDidBecomeActive(notification: Notification) { - guard sendApplicationOpenedOnDidBecomeActive else { + @objc + func applicationDidEnterBackground(notification: Notification) { + guard let amplitude = amplitude else { return } - sendApplicationOpenedOnDidBecomeActive = false - - let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000) - amplitude?.onEnterForeground(timestamp: timestamp) - sendApplicationOpened(fromBackground: false) + amplitude.onExitForeground(timestamp: currentTimestamp) + if amplitude.configuration.autocapture.contains(.appLifecycles) { + amplitude.track(eventType: Constants.AMP_APPLICATION_BACKGROUNDED_EVENT) + } } - private func sendApplicationOpened(fromBackground: Bool) { - guard amplitude?.configuration.autocapture.contains(.appLifecycles) ?? false else { - return - } - let info = Bundle.main.infoDictionary - let currentBuild = info?["CFBundleVersion"] as? String - let currentVersion = info?["CFBundleShortVersionString"] as? String - self.amplitude?.track(eventType: Constants.AMP_APPLICATION_OPENED_EVENT, eventProperties: [ - Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "", - Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "", - Constants.AMP_APP_FROM_BACKGROUND_PROPERTY: fromBackground, - ]) + private var currentTimestamp: Int64 { + return Int64(NSDate().timeIntervalSince1970 * 1000) } } diff --git a/Sources/Amplitude/Utilities/DefaultEventUtils.swift b/Sources/Amplitude/Utilities/DefaultEventUtils.swift index 5e78a18..9c04269 100644 --- a/Sources/Amplitude/Utilities/DefaultEventUtils.swift +++ b/Sources/Amplitude/Utilities/DefaultEventUtils.swift @@ -1,6 +1,9 @@ import Foundation public class DefaultEventUtils { + + private static var instanceNamesThatSentAppUpdatedInstalled: Set = [] + private weak var amplitude: Amplitude? public init(amplitude: Amplitude) { @@ -8,35 +11,59 @@ public class DefaultEventUtils { } public func trackAppUpdatedInstalledEvent() { + guard let amplitude = amplitude else { + return + } + let info = Bundle.main.infoDictionary let currentBuild = info?["CFBundleVersion"] as? String let currentVersion = info?["CFBundleShortVersionString"] as? String - let previousBuild: String? = amplitude?.storage.read(key: StorageKey.APP_BUILD) - let previousVersion: String? = amplitude?.storage.read(key: StorageKey.APP_VERSION) - - if amplitude?.configuration.autocapture.contains(.appLifecycles) ?? false { - let lastEventTime: Int64? = amplitude?.storage.read(key: StorageKey.LAST_EVENT_TIME) - if lastEventTime == nil { - self.amplitude?.track(eventType: Constants.AMP_APPLICATION_INSTALLED_EVENT, eventProperties: [ - Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "", - Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "", - ]) - } else if currentBuild != previousBuild { - self.amplitude?.track(eventType: Constants.AMP_APPLICATION_UPDATED_EVENT, eventProperties: [ - Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "", - Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "", - Constants.AMP_APP_PREVIOUS_BUILD_PROPERTY: previousBuild ?? "", - Constants.AMP_APP_PREVIOUS_VERSION_PROPERTY: previousVersion ?? "", - ]) - } - } + let previousBuild: String? = amplitude.storage.read(key: StorageKey.APP_BUILD) + let previousVersion: String? = amplitude.storage.read(key: StorageKey.APP_VERSION) if currentBuild != previousBuild { - try? amplitude?.storage.write(key: StorageKey.APP_BUILD, value: currentBuild) + try? amplitude.storage.write(key: StorageKey.APP_BUILD, value: currentBuild) } if currentVersion != previousVersion { - try? amplitude?.storage.write(key: StorageKey.APP_VERSION, value: currentVersion) + try? amplitude.storage.write(key: StorageKey.APP_VERSION, value: currentVersion) + } + + guard amplitude.configuration.autocapture.contains(.appLifecycles), + !Self.instanceNamesThatSentAppUpdatedInstalled.contains(amplitude.configuration.instanceName) else { + return + } + // Only send one app installed / updated event per instance name, no matter how many times we are + // reinitialized + Self.instanceNamesThatSentAppUpdatedInstalled.insert(amplitude.configuration.instanceName) + + if previousBuild == nil || previousVersion == nil { + amplitude.track(eventType: Constants.AMP_APPLICATION_INSTALLED_EVENT, eventProperties: [ + Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "", + Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "", + ]) + } else if currentBuild != previousBuild || currentVersion != previousVersion { + amplitude.track(eventType: Constants.AMP_APPLICATION_UPDATED_EVENT, eventProperties: [ + Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "", + Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "", + Constants.AMP_APP_PREVIOUS_BUILD_PROPERTY: previousBuild ?? "", + Constants.AMP_APP_PREVIOUS_VERSION_PROPERTY: previousVersion ?? "", + ]) } } + func trackAppOpenedEvent(fromBackground: Bool = false) { + guard let amplitude = amplitude, + amplitude.configuration.autocapture.contains(.appLifecycles) else { + return + } + + let info = Bundle.main.infoDictionary + let currentBuild = info?["CFBundleVersion"] as? String + let currentVersion = info?["CFBundleShortVersionString"] as? String + self.amplitude?.track(eventType: Constants.AMP_APPLICATION_OPENED_EVENT, eventProperties: [ + Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "", + Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "", + Constants.AMP_APP_FROM_BACKGROUND_PROPERTY: fromBackground, + ]) + } } diff --git a/Tests/AmplitudeTests/AmplitudeIOSTests.swift b/Tests/AmplitudeTests/AmplitudeIOSTests.swift index 8a456f4..f625308 100644 --- a/Tests/AmplitudeTests/AmplitudeIOSTests.swift +++ b/Tests/AmplitudeTests/AmplitudeIOSTests.swift @@ -19,14 +19,17 @@ final class AmplitudeIOSTests: XCTestCase { window.addSubview(rootViewController.view) } - func testDidFinishLaunching_ApplicationInstalled() { + func testDidFinishLaunching_ApplicationInstalled() throws { let configuration = Configuration( apiKey: "api-key", + instanceName: #function, storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, autocapture: .appLifecycles ) let amplitude = Amplitude(configuration: configuration) + try storageMem.write(key: StorageKey.APP_BUILD, value: nil) + try storageMem.write(key: StorageKey.APP_VERSION, value: nil) NotificationCenter.default.post(name: UIApplication.didFinishLaunchingNotification, object: nil) amplitude.waitForTrackingQueue() @@ -47,11 +50,11 @@ final class AmplitudeIOSTests: XCTestCase { func testDidFinishLaunching_ApplicationUpdated() throws { let configuration = Configuration( apiKey: "api-key", + instanceName: #function, storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, autocapture: .appLifecycles ) - try storageMem.write(key: StorageKey.LAST_EVENT_TIME, value: 123 as Int64) try storageMem.write(key: StorageKey.APP_BUILD, value: "abc") try storageMem.write(key: StorageKey.APP_VERSION, value: "xyz") let amplitude = Amplitude(configuration: configuration)