Loading...
Loading...
Compare original and translation side by side
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 AppDelegateWhat'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 AppDelegateWhat 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, overnightWhat 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, overnightWhat 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 wayWhat 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 wayWhat'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 firstWhat'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// ❌ 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
}// ❌ 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
}// ❌ 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
}// ❌ 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
}// ❌ 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)
}
}// ❌ 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)")
}
}// ❌ 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)
}
}// ❌ 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)")
}
}// ❌ 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()
}// ❌ 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
}// ❌ 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()
}// ❌ 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
}// ❌ 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)
}// ❌ 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)
}@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
}
}
}@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
}
}
}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)
}
}
}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)
}
}
}@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()
}
}
}@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()
}
}
}| 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 |
| 事件 | 触发时机 | 用途 |
|---|---|---|
| willFinishLaunching | UI加载前 | 仅用于崩溃上报 |
| didFinishLaunching | UI就绪后 | 关键初始化操作 |
| sceneWillEnterForeground | 应用即将回到前台 | 撤销后台状态变更 |
| sceneDidBecomeActive | 应用完全激活 | 刷新数据、重启任务 |
| sceneWillResignActive | 应用即将失去焦点 | 暂停媒体播放 |
| sceneDidEnterBackground | 应用进入后台 | 保存状态、启动后台任务 |
| sceneDidDisconnect | 场景被释放 | 保存场景状态 |
| 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 |
| 任务类型 | 时间限制 | 运行时机 |
|---|---|---|
| beginBackgroundTask | ~30秒 | 立即执行 |
| BGAppRefreshTask | ~30秒 | 系统调度 |
| BGProcessingTask | 数分钟 | 充电时、夜间 |
| Background URL Session | 无限制 | 系统管理 |
| Approach | Scope | Types | Auto-save |
|---|---|---|---|
| @SceneStorage | Per-scene | Codable | Yes |
| @AppStorage | App-wide | Primitives | Yes |
| Restoration ID | Per-VC | Custom | Manual |
| 方案 | 作用范围 | 支持类型 | 自动保存 |
|---|---|---|---|
| @SceneStorage | 单场景 | 仅Codable类型 | 是 |
| @AppStorage | 全应用 | 基本数据类型 | 是 |
| 恢复ID | 单个视图控制器 | 自定义类型 | 手动 |
| 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 |
| 问题表现 | 潜在风险 | 修复方案 |
|---|---|---|
| 启动时同步网络请求 | UI阻塞 | 异步加载+骨架屏 |
| 所有SDK都在didFinish中初始化 | 启动缓慢 | 优先级划分+延迟初始化 |
| 未使用beginBackgroundTask | 后台任务可能无法完成 | 始终申请后台执行时间 |
| 未结束后台任务 | 任务泄漏导致应用被终止 | 使用defer确保任务结束 |
| willResignActive中执行繁重工作 | 应用切换动画卡顿 | 移至didEnterBackground执行 |
| 依赖applicationWillTerminate保存数据 | 数据可能丢失 | 在进入后台时保存数据 |
| 混淆sceneDidDisconnect与应用终止 | 错误清理资源 | 仅保存场景状态 |