.notRunning
private var cancellables = Set<AnyCancellable>()
public static let shared = AppLifecycleManager()
private init() {
setupObservers()
}
private func setupObservers() {
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
.map { _ in AppLifecycleState.background }
.assign(to: \.currentState, on: self)
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.map { _ in AppLifecycleState.active }
.assign(to: \.currentState, on: self)
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)
.map { _ in AppLifecycleState.inactive }
.assign(to: \.currentState, on: self)
.store(in: &cancellables)
}
// Expose state changes for synchronous checks if needed
public func isForegroundActive() -> Bool {
currentState == .active
}
}
### 2. Scene-Based Lifecycle Implementation
For iOS 13 and later, `UISceneDelegate` is mandatory. The architecture must handle multiple scenes if the app supports multitasking. The `SceneDelegate` should forward lifecycle events to the manager and handle scene-specific restoration.
```swift
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
// Initialize window and root view controller
let window = UIWindow(windowScene: windowScene)
let viewModel = MainViewModel()
window.rootViewController = UIHostingController(rootView: MainView(viewModel: viewModel))
self.window = window
window.makeKeyAndVisible()
// Register for restoration
session.setRestorableState(viewModel)
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called when the scene is being released by the system.
// Release any resources associated with this scene.
AppLifecycleManager.shared.handleSceneDisconnect(scene)
}
func sceneDidBecomeActive(_ scene: UIScene) {
AppLifecycleManager.shared.notifySceneActive(scene)
}
func sceneWillResignActive(_ scene: UIScene) {
AppLifecycleManager.shared.notifySceneResignActive(scene)
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from background to foreground.
AppLifecycleManager.shared.notifySceneWillEnterForeground(scene)
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Trigger state preservation and background tasks
AppLifecycleManager.shared.notifySceneDidEnterBackground(scene)
scheduleBackgroundTasks()
}
private func scheduleBackgroundTasks() {
let request = BGAppRefreshTaskRequest(identifier: "com.codcompass.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins
try? BGTaskScheduler.shared.submit(request)
}
}
3. State Preservation and Restoration
State preservation must occur immediately upon entering the background. Use UIStateRestoring protocol for view controllers and models that need restoration.
import UIKit
class MainViewModel: ObservableObject, UIStateRestoring {
@Published var selectedTab: Int = 0
@Published var draftContent: String = ""
// MARK: - UIStateRestoring
func encodeRestorableState(with coder: NSCoder) {
coder.encode(selectedTab, forKey: "selectedTab")
coder.encode(draftContent, forKey: "draftContent")
}
func decodeRestorableState(with coder: NSCoder) {
selectedTab = coder.decodeInteger(forKey: "selectedTab")
draftContent = coder.decodeObject(forKey: "draftContent") as? String ?? ""
}
static var restorationIdentifier: String {
return "MainViewModel"
}
}
4. Background Task Scheduler Integration
Modern iOS apps must use BGTaskScheduler for periodic updates, network refreshes, and processing. Register tasks in AppDelegate and implement handlers.
import UIKit
import BackgroundTasks
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerBackgroundTasks()
return true
}
private func registerBackgroundTasks() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.codcompass.app.refresh", using: nil) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
}
private func handleAppRefresh(task: BGAppRefreshTask) {
// Schedule next refresh
let request = BGAppRefreshTaskRequest(identifier: "com.codcompass.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
// Perform fetch
let operation = NetworkFetchOperation()
operation.completionBlock = {
task.setTaskCompleted(success: !operation.hasError)
}
OperationQueue.main.addOperation(operation)
// Schedule expiration handler
task.expirationHandler = {
operation.cancel()
}
}
}
Architecture Decisions
- Observable Pattern: Using
@Published in AppLifecycleManager allows SwiftUI views and Combine subscribers to react to lifecycle changes without tight coupling.
- Separation of Concerns:
SceneDelegate handles scene-specific UI setup, while AppLifecycleManager handles global state. This prevents UI logic from leaking into lifecycle callbacks.
- State Restoration Keys: Using explicit keys in
encodeRestorableState ensures data integrity during restoration, avoiding reliance on implicit ordering.
- Background Task Scheduling: Tasks are scheduled in
sceneDidEnterBackground to ensure the system grants execution time. The earliestBeginDate prevents aggressive scheduling that could lead to system penalties.
Pitfall Guide
- Assuming
applicationWillTerminate is Called: The system rarely calls this method. It is invoked only when the app is running in the foreground and the user force-quits it. Apps must save critical state in sceneDidEnterBackground or applicationDidEnterBackground, not in willTerminate.
- Blocking the Main Thread in
didFinishLaunching: Long-running initialization tasks in didFinishLaunching delay the app's appearance, leading to watchdog terminations. Offload non-critical initialization to background queues and use a loading screen or skeleton UI.
- Ignoring
applicationSignificantTimeChange: This notification fires when the system time changes significantly (e.g., carrier time update, manual change). Failing to handle this can cause timer drift and scheduled task misalignment.
- Leaking Resources in
DidEnterBackground: Failing to invalidate timers, close database connections, or release large memory allocations when entering the background increases the likelihood of the app being purged by the system. Always implement cleanup logic.
- Mishandling
UISceneSession Discard: When supporting multiple scenes, failing to implement scene:didDiscardSceneSessions can lead to memory leaks. The system may discard sessions without explicit user action; ensure resources are released.
- Misusing SwiftUI
onAppear/onDisappear: These modifiers relate to view visibility, not app lifecycle. Relying on them for critical lifecycle logic (like pausing video) can result in incorrect behavior when the app is backgrounded but the view remains in memory. Use @Environment(\.scenePhase) or AppLifecycleManager.
- Not Handling Memory Warnings:
didReceiveMemoryWarning is sent when the system is low on memory. Apps must purge caches and release non-essential resources. Ignoring this leads to termination. Implement logic to reduce memory footprint proactively.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple Utility App | UIApplicationDelegate + NotificationCenter | Low complexity; scene support is unnecessary overhead. | Low |
| Complex App with Multitasking | UISceneDelegate + AppLifecycleManager | Required for iPadOS multitasking; ensures state isolation per scene. | Medium |
| App with Frequent Background Sync | BGTaskScheduler + AppLifecycleManager | System-enforced scheduling prevents battery drain and termination. | Medium |
| SwiftUI-Only App | @main Struct + @Environment(\.scenePhase) | Native SwiftUI lifecycle integration; reduces boilerplate. | Low |
| Hybrid UIKit/SwiftUI | AppLifecycleManager bridging both | Provides unified state source for mixed technology stack. | High |
Configuration Template
Copy this template to configure background tasks and lifecycle keys in Info.plist and BGTaskScheduler.
Info.plist:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.codcompass.app.refresh</string>
<string>com.codcompass.app.process</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
LifecycleManager Extension for SwiftUI:
import SwiftUI
extension AppLifecycleManager {
func scenePhaseBinding() -> Binding<AppLifecycleState> {
Binding(
get: { self.currentState },
set: { _ in } // Read-only for views
)
}
}
// Usage in SwiftUI View
struct MyView: View {
@EnvironmentObject var lifecycleManager: AppLifecycleManager
var body: some View {
Text("State: \(lifecycleManager.currentState)")
.onReceive(lifecycleManager.$currentState) { state in
if state == .background {
pauseActivity()
} else if state == .active {
resumeActivity()
}
}
}
}
Quick Start Guide
- Define State Enum: Create
AppLifecycleState enum covering all critical states: .notRunning, .inactive, .active, .background, .suspended.
- Initialize Manager: Implement
AppLifecycleManager as a singleton with @Published state and NotificationCenter observers for UIApplication lifecycle notifications.
- Hook SceneDelegate: In
SceneDelegate, forward sceneDidEnterBackground and sceneDidBecomeActive to the manager. Schedule BGTaskScheduler requests in the background transition.
- Inject and Observe: Inject
AppLifecycleManager into your dependency container. In SwiftUI, use @EnvironmentObject; in UIKit, subscribe to objectWillChange or use Combine.
- Verify: Run the app in Xcode, use the Debug Memory Graph to check for leaks on background transition, and use
simulateBackgroundFetch to validate background task execution.