home / skills / kaakati / rails-enterprise-dev / app-lifecycle
This skill guides iOS app lifecycle decisions, balancing SwiftUI lifecycle, background tasks, and launch optimizations to improve reliability and user
npx playbooks add skill kaakati/rails-enterprise-dev --skill app-lifecycleReview the files below or copy the command above to add this skill to your agents.
---
name: app-lifecycle
description: "Expert lifecycle decisions for iOS/tvOS: when SwiftUI lifecycle vs SceneDelegate, background task strategies, state restoration trade-offs, and launch optimization. Use when managing app state transitions, handling background work, or debugging lifecycle issues. Trigger keywords: lifecycle, scenePhase, SceneDelegate, AppDelegate, background task, state restoration, launch time, didFinishLaunching, applicationWillTerminate, sceneDidBecomeActive"
version: "3.0.0"
---
# App Lifecycle — Expert Decisions
Expert decision frameworks for app lifecycle choices. Claude knows scenePhase and SceneDelegate — this skill provides judgment calls for architecture decisions and background task trade-offs.
---
## Decision Trees
### Lifecycle API Selection
```
What's your project setup?
├─ Pure SwiftUI app (iOS 14+)
│ └─ @main App + scenePhase
│ Simplest approach, sufficient for most apps
│
├─ Need UIKit integration
│ └─ SceneDelegate + UIHostingController
│ Required for some third-party SDKs
│
├─ Need pre-launch setup
│ └─ AppDelegate + SceneDelegate
│ SDK initialization, remote notifications
│
└─ Legacy app (pre-iOS 13)
└─ AppDelegate only
window property on AppDelegate
```
**The trap**: Using SceneDelegate when pure SwiftUI suffices. scenePhase covers most use cases without the boilerplate.
### Background Task Strategy
```
What work needs to happen in background?
├─ Quick save (< 5 seconds)
│ └─ UIApplication.beginBackgroundTask
│ Request extra time in sceneDidEnterBackground
│
├─ Network sync (< 30 seconds)
│ └─ BGAppRefreshTask
│ System schedules, best-effort timing
│
├─ Large download/upload
│ └─ Background URL Session
│ Continues even after app termination
│
├─ Location tracking
│ └─ Location background mode
│ Significant change or continuous
│
└─ Long processing (> 30 seconds)
└─ BGProcessingTask
Runs during charging, overnight
```
### State Restoration Approach
```
What state needs restoration?
├─ Simple navigation state
│ └─ @SceneStorage
│ Per-scene, automatic, Codable types only
│
├─ Complex navigation + data
│ └─ @AppStorage + manual encoding
│ More control, cross-scene sharing
│
├─ UIKit-based navigation
│ └─ State restoration identifiers
│ encodeRestorableState/decodeRestorableState
│
└─ Don't need restoration
└─ Start fresh each launch
Some apps are better this way
```
### Launch Optimization Priority
```
What's blocking your launch time?
├─ SDK initialization
│ └─ Defer non-critical SDKs
│ Analytics can wait, auth cannot
│
├─ Database loading
│ └─ Lazy loading + skeleton UI
│ Show UI immediately, load data async
│
├─ Network requests
│ └─ Cache + background refresh
│ Never block launch for network
│
└─ Asset loading
└─ Progressive loading
Load visible content first
```
---
## NEVER Do
### Launch Time
**NEVER** block main thread during launch:
```swift
// ❌ UI frozen until network completes
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let data = try! Data(contentsOf: remoteURL) // Synchronous network!
processData(data)
return true
}
// ✅ Defer non-critical work
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
setupCriticalServices() // Auth, crash reporting
Task.detached(priority: .background) {
await self.setupNonCriticalServices() // Analytics, prefetch
}
return true
}
```
**NEVER** initialize all SDKs synchronously:
```swift
// ❌ Each SDK adds to launch time
func application(...) -> Bool {
AnalyticsSDK.initialize() // 100ms
CrashReporterSDK.initialize() // 50ms
FeatureFlagsSDK.initialize() // 200ms
SocialSDK.initialize() // 150ms
// Total: 500ms added to launch!
return true
}
// ✅ Prioritize and defer
func application(...) -> Bool {
CrashReporterSDK.initialize() // Critical — catches launch crashes
DispatchQueue.main.async {
AnalyticsSDK.initialize() // Can wait one runloop
}
Task.detached(priority: .utility) {
FeatureFlagsSDK.initialize()
SocialSDK.initialize()
}
return true
}
```
### Background Tasks
**NEVER** assume background time is guaranteed:
```swift
// ❌ May not complete — iOS can terminate anytime
func sceneDidEnterBackground(_ scene: UIScene) {
performLongSync() // No protection!
}
// ✅ Request background time and handle expiration
func sceneDidEnterBackground(_ scene: UIScene) {
var taskId: UIBackgroundTaskIdentifier = .invalid
taskId = UIApplication.shared.beginBackgroundTask {
// Expiration handler — save partial progress
savePartialProgress()
UIApplication.shared.endBackgroundTask(taskId)
}
Task {
await performSync()
UIApplication.shared.endBackgroundTask(taskId)
}
}
```
**NEVER** forget to end background tasks:
```swift
// ❌ Leaks background task — iOS may terminate app
func saveData() {
let taskId = UIApplication.shared.beginBackgroundTask { }
saveToDatabase()
// Missing: endBackgroundTask!
}
// ✅ Always end in both success and failure
func saveData() {
var taskId: UIBackgroundTaskIdentifier = .invalid
taskId = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(taskId)
}
defer { UIApplication.shared.endBackgroundTask(taskId) }
do {
try saveToDatabase()
} catch {
Logger.app.error("Save failed: \(error)")
}
}
```
### State Transitions
**NEVER** trust applicationWillTerminate to be called:
```swift
// ❌ May never be called — iOS can kill app without notice
func applicationWillTerminate(_ application: UIApplication) {
saveCriticalData() // Not guaranteed to run!
}
// ✅ Save on every background transition
func sceneDidEnterBackground(_ scene: UIScene) {
saveCriticalData() // Called reliably
}
// Also save periodically during use
Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in
saveApplicationState()
}
```
**NEVER** do heavy work in sceneWillResignActive:
```swift
// ❌ Blocks app switcher animation
func sceneWillResignActive(_ scene: UIScene) {
generateThumbnails() // Visible lag in app switcher
syncToServer() // Delays user
}
// ✅ Only pause essential operations
func sceneWillResignActive(_ scene: UIScene) {
pauseVideoPlayback()
pauseAnimations()
// Heavy work goes in sceneDidEnterBackground
}
```
### Scene Lifecycle
**NEVER** confuse scene disconnect with app termination:
```swift
// ❌ Wrong assumption
func sceneDidDisconnect(_ scene: UIScene) {
// App is terminating! <- WRONG
cleanupEverything()
}
// ✅ Scene disconnect means scene released, not app death
func sceneDidDisconnect(_ scene: UIScene) {
// Scene being released — save per-scene state
// App may continue running with other scenes
// Or system may reconnect this scene later
saveSceneState(scene)
}
```
---
## Essential Patterns
### SwiftUI Lifecycle Handler
```swift
@main
struct MyApp: App {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
}
.onChange(of: scenePhase) { oldPhase, newPhase in
handlePhaseChange(from: oldPhase, to: newPhase)
}
}
private func handlePhaseChange(from old: ScenePhase, to new: ScenePhase) {
switch (old, new) {
case (_, .active):
appState.refreshDataIfStale()
case (.active, .inactive):
// Transitioning away — pause but don't save yet
appState.pauseActiveOperations()
case (_, .background):
appState.saveState()
scheduleBackgroundRefresh()
default:
break
}
}
}
```
### Background Task Manager
```swift
final class BackgroundTaskManager {
static let shared = BackgroundTaskManager()
func registerTasks() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.app.refresh",
using: nil
) { task in
self.handleAppRefresh(task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.app.processing",
using: nil
) { task in
self.handleProcessing(task as! BGProcessingTask)
}
}
func scheduleRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
private func handleAppRefresh(_ task: BGAppRefreshTask) {
scheduleRefresh() // Schedule next refresh
let refreshTask = Task {
await performRefresh()
}
task.expirationHandler = {
refreshTask.cancel()
}
Task {
await refreshTask.value
task.setTaskCompleted(success: true)
}
}
}
```
### Launch Time Optimization
```swift
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
private var launchStartTime: CFAbsoluteTime = 0
func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
launchStartTime = CFAbsoluteTimeGetCurrent()
// Phase 1: Absolute minimum (crash reporting)
CrashReporter.initialize()
return true
}
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Phase 2: Required for first frame
configureAppearance()
// Phase 3: Deferred to after first frame
DispatchQueue.main.async {
self.completePostLaunchSetup()
let launchTime = CFAbsoluteTimeGetCurrent() - self.launchStartTime
Logger.app.info("Launch completed in \(launchTime)s")
}
return true
}
private func completePostLaunchSetup() {
// Analytics, feature flags, etc.
Task.detached(priority: .utility) {
Analytics.initialize()
FeatureFlags.refresh()
}
}
}
```
---
## Quick Reference
### Lifecycle Events Order
| Event | When | Use For |
|-------|------|---------|
| willFinishLaunching | Before UI | Crash reporting only |
| didFinishLaunching | UI ready | Critical setup |
| sceneWillEnterForeground | Coming to front | Undo background changes |
| sceneDidBecomeActive | Fully active | Refresh, restart tasks |
| sceneWillResignActive | Losing focus | Pause playback |
| sceneDidEnterBackground | In background | Save state, start bg task |
| sceneDidDisconnect | Scene released | Save scene state |
### Background Task Limits
| Task Type | Time Limit | When Runs |
|-----------|-----------|-----------|
| beginBackgroundTask | ~30 seconds | Immediately |
| BGAppRefreshTask | ~30 seconds | System discretion |
| BGProcessingTask | Minutes | Charging, overnight |
| Background URL Session | Unlimited | System managed |
### State Restoration Options
| Approach | Scope | Types | Auto-save |
|----------|-------|-------|-----------|
| @SceneStorage | Per-scene | Codable | Yes |
| @AppStorage | App-wide | Primitives | Yes |
| Restoration ID | Per-VC | Custom | Manual |
### Red Flags
| Smell | Problem | Fix |
|-------|---------|-----|
| Sync network in launch | Blocked UI | Async + skeleton UI |
| All SDKs in didFinish | Slow launch | Prioritize + defer |
| No beginBackgroundTask | Work may not complete | Always request time |
| Missing endBackgroundTask | Leaked task | Use defer |
| Heavy work in willResignActive | Laggy app switcher | Move to didEnterBackground |
| Trust applicationWillTerminate | May not be called | Save on background |
| Confuse sceneDidDisconnect | Scene != app termination | Save scene state only |
This skill advises on iOS/tvOS app lifecycle decisions: when to use the SwiftUI App lifecycle vs SceneDelegate/AppDelegate, background task strategies, state restoration trade-offs, and launch optimizations. It condenses decision trees, never-do rules, and essential patterns into practical guidance for engineers managing app state transitions, background work, or launch time concerns.
It inspects your app surface (pure SwiftUI, UIKit integration needs, legacy targets) and maps that to an appropriate lifecycle API choice. It recommends background execution patterns (beginBackgroundTask, BGAppRefreshTask, BGProcessingTask, background URL sessions) and provides concrete rules for saving state, deferring SDKs, and measuring launch time. The skill highlights reliable lifecycle hooks and failure modes to avoid.
When should I prefer SceneDelegate over the SwiftUI App lifecycle?
Use SceneDelegate when you need deeper UIKit integration or third-party SDKs that require window/scene-level hooks; for pure SwiftUI apps on iOS 14+, prefer @main with scenePhase.
How long can I expect beginBackgroundTask to run?
beginBackgroundTask typically grants on the order of ~30 seconds; for longer work use BGProcessingTask or background URL sessions depending on the job type.