Skip to content
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
16 changes: 13 additions & 3 deletions Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle {
@Atomic
private var didFinishLaunching = false

@Atomic
private var wasBackgrounded = false

func application(_ application: UIApplication?, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
// Make sure we aren't double calling application:didFinishLaunchingWithOptions
// by resetting the check at the start
Expand Down Expand Up @@ -83,19 +86,26 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle {
])
}
}

// Only fire if we were actually backgrounded
if wasBackgrounded {
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationForegrounded) == true {
analytics?.track(name: "Application Foregrounded")
}
_wasBackgrounded.set(false)
}
}

func applicationDidEnterBackground(application: UIApplication?) {
_didFinishLaunching.set(false)
_wasBackgrounded.set(true)
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationBackgrounded) == true {
analytics?.track(name: "Application Backgrounded")
}
}

func applicationDidBecomeActive(application: UIApplication?) {
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationForegrounded) == true {
analytics?.track(name: "Application Foregrounded")
}
// DO NOT USE THIS.
}

private func urlFrom(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> String {
Expand Down
148 changes: 148 additions & 0 deletions Tests/Segment-Tests/iOSLifecycle_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,154 @@ final class iOSLifecycle_Tests: XCTestCase {
XCTAssertTrue(trackEvent?.event == "Application Opened")
XCTAssertTrue(trackEvent?.type == "track")
}

func testApplicationForegroundedOnlyFiresAfterBackground() {
let analytics = Analytics(configuration: Configuration(writeKey: "test")
.setTrackedApplicationLifecycleEvents(.all))
let outputReader = OutputReaderPlugin()
analytics.add(plugin: outputReader)

waitUntilStarted(analytics: analytics)

// Simulate: Background → Foreground
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)

let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
XCTAssertEqual(trackEvent?.event, "Application Foregrounded",
"Application Foregrounded should fire after coming back from background")
}

func testTransientInterruptionDoesNotFireForegrounded() {
let analytics = Analytics(configuration: Configuration(writeKey: "test")
.setTrackedApplicationLifecycleEvents(.all))
let outputReader = OutputReaderPlugin()
analytics.add(plugin: outputReader)

waitUntilStarted(analytics: analytics)

// Clear any startup events by capturing the current state
let eventsBeforeInterruption = outputReader.lastEvent

// Simulate: willResignActive → didBecomeActive (notification center, control center, etc.)
NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)

// lastEvent should still be the same as before (no new "Application Foregrounded")
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
if trackEvent?.event == "Application Foregrounded" {
XCTFail("Application Foregrounded should NOT fire for transient interruptions like notification center")
}
}

func testForegroundedNotFiredWithoutPriorBackground() {
let analytics = Analytics(configuration: Configuration(writeKey: "test")
.setTrackedApplicationLifecycleEvents(.all))
let outputReader = OutputReaderPlugin()
analytics.add(plugin: outputReader)

waitUntilStarted(analytics: analytics)

// Simulate: willEnterForeground without prior didEnterBackground
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)

let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
XCTAssertNotEqual(trackEvent?.event, "Application Foregrounded",
"Application Foregrounded should not fire without a prior background event")
}

func testMultipleBackgroundForegroundCycles() {
let analytics = Analytics(configuration: Configuration(writeKey: "test")
.setTrackedApplicationLifecycleEvents(.all))
let outputReader = OutputReaderPlugin()
analytics.add(plugin: outputReader)

waitUntilStarted(analytics: analytics)

// Cycle 1: Background → Foreground
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)

var trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
XCTAssertEqual(trackEvent?.event, "Application Foregrounded",
"First foreground cycle should fire Application Foregrounded")

// Cycle 2: Background → Foreground
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)

trackEvent = outputReader.lastEvent as? TrackEvent
XCTAssertEqual(trackEvent?.event, "Application Foregrounded",
"Second foreground cycle should also fire Application Foregrounded")
}

func testBackgroundAlwaysFires() {
let analytics = Analytics(configuration: Configuration(writeKey: "test")
.setTrackedApplicationLifecycleEvents(.all))
let outputReader = OutputReaderPlugin()
analytics.add(plugin: outputReader)

waitUntilStarted(analytics: analytics)

// Simulate: Background
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)

let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
XCTAssertEqual(trackEvent?.event, "Application Backgrounded",
"Application Backgrounded should always fire when app enters background")
}

func testComplexLifecycleSequence() {
let analytics = Analytics(configuration: Configuration(writeKey: "test")
.setTrackedApplicationLifecycleEvents(.all))
let outputReader = OutputReaderPlugin()
analytics.add(plugin: outputReader)

waitUntilStarted(analytics: analytics)

// Simulate realistic user behavior:
// 1. Background the app
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
var trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
XCTAssertEqual(trackEvent?.event, "Application Backgrounded")

// 2. Foreground the app
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)
trackEvent = outputReader.lastEvent as? TrackEvent
XCTAssertEqual(trackEvent?.event, "Application Foregrounded")

// 3. Pull down notification center (transient interruption)
NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)

// Last event should still be "Application Foregrounded" from step 2
trackEvent = outputReader.lastEvent as? TrackEvent
XCTAssertEqual(trackEvent?.event, "Application Foregrounded",
"Transient interruption should not create new events")

// 4. Background again
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
trackEvent = outputReader.lastEvent as? TrackEvent
XCTAssertEqual(trackEvent?.event, "Application Backgrounded")
}

func testDidBecomeActiveDoesNotFireForegrounded() {
let analytics = Analytics(configuration: Configuration(writeKey: "test")
.setTrackedApplicationLifecycleEvents(.all))
let outputReader = OutputReaderPlugin()
analytics.add(plugin: outputReader)

waitUntilStarted(analytics: analytics)

// Simulate: didBecomeActive (should not fire anything anymore)
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)

// Verify no new event was created
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
if trackEvent?.event == "Application Foregrounded" {
XCTFail("didBecomeActive should not fire Application Foregrounded anymore")
}
}
}

#endif