AppLifecycleObserver
Observes UIApplication lifecycle notifications -- active, inactive, and background transitions.
Model
public enum AppLifecycleState: Sendable {
/// The app is in the foreground and receiving events.
case active
/// The app is in the foreground but not receiving events.
case inactive
/// The app is in the background.
case background
} Protocol
public protocol AppLifecycleObserving: Sendable {
/// The current lifecycle state.
var state: AppLifecycleState { get }
/// An AsyncStream that emits lifecycle state changes.
var stateStream: AsyncStream<AppLifecycleState> { get }
} Usage
let lifecycle = AppLifecycleObserver()
// Read current state
if lifecycle.state == .active {
startTimer()
}
// Refresh data when the app becomes active
for await state in lifecycle.stateStream {
if state == .active {
await refreshData()
}
} // Save state when entering background
for await state in lifecycle.stateStream {
switch state {
case .active:
resumeTracking()
case .inactive:
break // transitioning, usually ignore
case .background:
await saveState()
pauseTracking()
}
} Testing
The observer accepts an injected NotificationCenter so tests can pump fake lifecycle notifications without touching the real UIApplication.
import UIKit
import ForgeObservers
@Test
func lifecycleObserverReactsToBackground() async throws {
// Inject a fresh NotificationCenter — no interference from the real one
let center = NotificationCenter()
let observer = AppLifecycleObserver(notificationCenter: center)
// Post the notification UIApplication would post in production
center.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
try await Task.sleep(for: .milliseconds(50)) // let the .main queue deliver
#expect(observer.state == .background)
} Why the sleep: the observer registers its callbacks on .main queue, so notifications posted synchronously are delivered asynchronously. Yielding for ~50 ms gives the main run loop time to drain.