Skip to content

Commit 7f300e0

Browse files
committed
Reworked ios app lifecycle events to be less noisy.
1 parent 20251c7 commit 7f300e0

File tree

2 files changed

+161
-3
lines changed

2 files changed

+161
-3
lines changed

Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle {
2424
@Atomic
2525
private var didFinishLaunching = false
2626

27+
@Atomic
28+
private var wasBackgrounded = false
29+
2730
func application(_ application: UIApplication?, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
2831
// Make sure we aren't double calling application:didFinishLaunchingWithOptions
2932
// by resetting the check at the start
@@ -83,19 +86,26 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle {
8386
])
8487
}
8588
}
89+
90+
// Only fire if we were actually backgrounded
91+
if wasBackgrounded {
92+
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationForegrounded) == true {
93+
analytics?.track(name: "Application Foregrounded")
94+
}
95+
_wasBackgrounded.set(false)
96+
}
8697
}
8798

8899
func applicationDidEnterBackground(application: UIApplication?) {
89100
_didFinishLaunching.set(false)
101+
_wasBackgrounded.set(true)
90102
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationBackgrounded) == true {
91103
analytics?.track(name: "Application Backgrounded")
92104
}
93105
}
94106

95107
func applicationDidBecomeActive(application: UIApplication?) {
96-
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationForegrounded) == true {
97-
analytics?.track(name: "Application Foregrounded")
98-
}
108+
// DO NOT USE THIS.
99109
}
100110

101111
private func urlFrom(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> String {

Tests/Segment-Tests/iOSLifecycle_Tests.swift

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,154 @@ final class iOSLifecycle_Tests: XCTestCase {
6767
XCTAssertTrue(trackEvent?.event == "Application Opened")
6868
XCTAssertTrue(trackEvent?.type == "track")
6969
}
70+
71+
func testApplicationForegroundedOnlyFiresAfterBackground() {
72+
let analytics = Analytics(configuration: Configuration(writeKey: "test")
73+
.setTrackedApplicationLifecycleEvents(.all))
74+
let outputReader = OutputReaderPlugin()
75+
analytics.add(plugin: outputReader)
76+
77+
waitUntilStarted(analytics: analytics)
78+
79+
// Simulate: Background → Foreground
80+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
81+
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)
82+
83+
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
84+
XCTAssertEqual(trackEvent?.event, "Application Foregrounded",
85+
"Application Foregrounded should fire after coming back from background")
86+
}
87+
88+
func testTransientInterruptionDoesNotFireForegrounded() {
89+
let analytics = Analytics(configuration: Configuration(writeKey: "test")
90+
.setTrackedApplicationLifecycleEvents(.all))
91+
let outputReader = OutputReaderPlugin()
92+
analytics.add(plugin: outputReader)
93+
94+
waitUntilStarted(analytics: analytics)
95+
96+
// Clear any startup events by capturing the current state
97+
let eventsBeforeInterruption = outputReader.lastEvent
98+
99+
// Simulate: willResignActive → didBecomeActive (notification center, control center, etc.)
100+
NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil)
101+
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)
102+
103+
// lastEvent should still be the same as before (no new "Application Foregrounded")
104+
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
105+
if trackEvent?.event == "Application Foregrounded" {
106+
XCTFail("Application Foregrounded should NOT fire for transient interruptions like notification center")
107+
}
108+
}
109+
110+
func testForegroundedNotFiredWithoutPriorBackground() {
111+
let analytics = Analytics(configuration: Configuration(writeKey: "test")
112+
.setTrackedApplicationLifecycleEvents(.all))
113+
let outputReader = OutputReaderPlugin()
114+
analytics.add(plugin: outputReader)
115+
116+
waitUntilStarted(analytics: analytics)
117+
118+
// Simulate: willEnterForeground without prior didEnterBackground
119+
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)
120+
121+
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
122+
XCTAssertNotEqual(trackEvent?.event, "Application Foregrounded",
123+
"Application Foregrounded should not fire without a prior background event")
124+
}
125+
126+
func testMultipleBackgroundForegroundCycles() {
127+
let analytics = Analytics(configuration: Configuration(writeKey: "test")
128+
.setTrackedApplicationLifecycleEvents(.all))
129+
let outputReader = OutputReaderPlugin()
130+
analytics.add(plugin: outputReader)
131+
132+
waitUntilStarted(analytics: analytics)
133+
134+
// Cycle 1: Background → Foreground
135+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
136+
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)
137+
138+
var trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
139+
XCTAssertEqual(trackEvent?.event, "Application Foregrounded",
140+
"First foreground cycle should fire Application Foregrounded")
141+
142+
// Cycle 2: Background → Foreground
143+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
144+
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)
145+
146+
trackEvent = outputReader.lastEvent as? TrackEvent
147+
XCTAssertEqual(trackEvent?.event, "Application Foregrounded",
148+
"Second foreground cycle should also fire Application Foregrounded")
149+
}
150+
151+
func testBackgroundAlwaysFires() {
152+
let analytics = Analytics(configuration: Configuration(writeKey: "test")
153+
.setTrackedApplicationLifecycleEvents(.all))
154+
let outputReader = OutputReaderPlugin()
155+
analytics.add(plugin: outputReader)
156+
157+
waitUntilStarted(analytics: analytics)
158+
159+
// Simulate: Background
160+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
161+
162+
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
163+
XCTAssertEqual(trackEvent?.event, "Application Backgrounded",
164+
"Application Backgrounded should always fire when app enters background")
165+
}
166+
167+
func testComplexLifecycleSequence() {
168+
let analytics = Analytics(configuration: Configuration(writeKey: "test")
169+
.setTrackedApplicationLifecycleEvents(.all))
170+
let outputReader = OutputReaderPlugin()
171+
analytics.add(plugin: outputReader)
172+
173+
waitUntilStarted(analytics: analytics)
174+
175+
// Simulate realistic user behavior:
176+
// 1. Background the app
177+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
178+
var trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
179+
XCTAssertEqual(trackEvent?.event, "Application Backgrounded")
180+
181+
// 2. Foreground the app
182+
NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)
183+
trackEvent = outputReader.lastEvent as? TrackEvent
184+
XCTAssertEqual(trackEvent?.event, "Application Foregrounded")
185+
186+
// 3. Pull down notification center (transient interruption)
187+
NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil)
188+
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)
189+
190+
// Last event should still be "Application Foregrounded" from step 2
191+
trackEvent = outputReader.lastEvent as? TrackEvent
192+
XCTAssertEqual(trackEvent?.event, "Application Foregrounded",
193+
"Transient interruption should not create new events")
194+
195+
// 4. Background again
196+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
197+
trackEvent = outputReader.lastEvent as? TrackEvent
198+
XCTAssertEqual(trackEvent?.event, "Application Backgrounded")
199+
}
200+
201+
func testDidBecomeActiveDoesNotFireForegrounded() {
202+
let analytics = Analytics(configuration: Configuration(writeKey: "test")
203+
.setTrackedApplicationLifecycleEvents(.all))
204+
let outputReader = OutputReaderPlugin()
205+
analytics.add(plugin: outputReader)
206+
207+
waitUntilStarted(analytics: analytics)
208+
209+
// Simulate: didBecomeActive (should not fire anything anymore)
210+
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)
211+
212+
// Verify no new event was created
213+
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
214+
if trackEvent?.event == "Application Foregrounded" {
215+
XCTFail("didBecomeActive should not fire Application Foregrounded anymore")
216+
}
217+
}
70218
}
71219

72220
#endif

0 commit comments

Comments
 (0)