FO
ForgeObservers

AppLifecycleObserver

Observes UIApplication lifecycle notifications -- active, inactive, and background transitions.

Model

AppLifecycleState
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

AppLifecycleObserving
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

Basic 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()
    }
}
Handling all states
// 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.

Test with injected NotificationCenter
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.