swift-app-lifecycle
SKILL.md
Swift App Lifecycle
Lifecycle Position
Phase 0-1 (Scaffold/Architecture). Load when setting up app entry point, scenes, and system integration. Related: macos-development for macOS-specific architecture patterns.
App Entry Point
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
#if os(macOS)
Settings {
SettingsView()
}
Window("About", id: "about") {
AboutView()
}
MenuBarExtra("Status", systemImage: "circle.fill") {
MenuBarView()
}
#endif
}
}
Scene Types
| Scene | Use Case | Platform |
|---|---|---|
WindowGroup |
Main app window (supports multiple instances) | All |
Window |
Single-instance utility window | macOS |
DocumentGroup |
Document-based app (open/save/new) | All |
Settings |
Preferences window (⌘,) | macOS |
MenuBarExtra |
Menu bar icon with popover or window | macOS |
WindowGroup with Value (macOS multi-window)
WindowGroup(for: Project.ID.self) { $projectID in
if let projectID {
ProjectView(id: projectID)
}
}
// Open from anywhere:
@Environment(\.openWindow) private var openWindow
Button("Open Project") { openWindow(value: project.id) }
ScenePhase Lifecycle
@Environment(\.scenePhase) private var scenePhase
.onChange(of: scenePhase) { oldPhase, newPhase in
switch newPhase {
case .active:
// App is visible and interactive
refreshData()
case .inactive:
// App is visible but not interactive (e.g., notification shade)
pauseTimers()
case .background:
// App is not visible — save state NOW
saveState()
scheduleBackgroundRefresh()
@unknown default: break
}
}
State preservation pattern:
@main
struct MyApp: App {
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
// For actor-based persistence, see swift-actor-persistence skill
PersistenceController.shared.save()
}
}
}
}
Deep Links
Custom URL Schemes
// Info.plist: Add URL Types → URL Schemes: "myapp"
// Handles: myapp://path/to/content?id=123
WindowGroup {
ContentView()
.onOpenURL { url in
router.handle(url)
}
}
// Router pattern
@Observable class DeepLinkRouter {
var selectedTab: Tab = .home
var presentedItem: Item?
func handle(_ url: URL) {
guard url.scheme == "myapp" else { return }
switch url.host {
case "item":
if let id = url.pathComponents.last {
presentedItem = Item(id: id)
}
case "settings":
selectedTab = .settings
default: break
}
}
}
Universal Links
// Requires Associated Domains entitlement: applinks:example.com
// apple-app-site-association file on your server
.onOpenURL { url in
// Handles both custom scheme AND https://example.com/item/123
router.handle(url)
}
Push Notifications
Permission Flow
import UserNotifications
func requestNotificationPermission() async -> Bool {
let center = UNUserNotificationCenter.current()
do {
let granted = try await center.requestAuthorization(options: [.alert, .badge, .sound])
if granted {
await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
}
return granted
} catch {
return false
}
}
Registration
// In AppDelegate adaptor:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
// Send token to your server
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Push registration failed: \(error)")
}
}
// Connect in App
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
// ...
}
Background Tasks
import BackgroundTasks
// Register in App init or AppDelegate
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.refresh", using: nil) { task in
handleAppRefresh(task: task as! BGAppRefreshTask)
}
// Schedule
func scheduleBackgroundRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 min
try? BGTaskScheduler.shared.submit(request)
}
// Handle
func handleAppRefresh(task: BGAppRefreshTask) {
scheduleBackgroundRefresh() // Schedule next one
let operation = Task {
await refreshContent()
}
task.expirationHandler = { operation.cancel() }
Task {
await operation.value
task.setTaskCompleted(success: true)
}
}
Info.plist requirement:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.app.refresh</string>
</array>
macOS-Specific
NSApplicationDelegateAdaptor
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { }
func applicationWillTerminate(_ notification: Notification) {
// Final cleanup
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
true // Quit when last window closes (document apps usually return false)
}
}
@main
struct MyApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
// ...
}
Menu Commands
var body: some Scene {
WindowGroup { ContentView() }
.commands {
CommandGroup(replacing: .newItem) {
Button("New Project") { /* ... */ }
.keyboardShortcut("n", modifiers: .command)
}
CommandMenu("Tools") {
Button("Run Analysis") { /* ... */ }
.keyboardShortcut("r", modifiers: [.command, .shift])
}
}
}
Common Mistakes
- Not saving state on
.background— data lost when system terminates backgrounded app - Requesting notification permission on launch — ask in context when user understands the value
- Missing
@UIApplicationDelegateAdaptor/@NSApplicationDelegateAdaptor— needed for push tokens and system callbacks - Background tasks not registered in Info.plist — silently fails to schedule
applicationShouldTerminateAfterLastWindowClosednot set on macOS — app stays running with no windows
Checklist
- State saved on
.backgroundscene phase transition - Deep link routes cover all app entry points
- Notification permissions requested contextually (not on first launch)
- Background tasks registered in Info.plist with matching identifiers
- macOS apps handle
applicationShouldTerminateAfterLastWindowClosed - Universal Links have apple-app-site-association file on server
-
@mainApp struct is the single entry point (no duplicate@main)
Templates
App lifecycle helpers in templates/ — copy and adapt:
ForceUpdateChecker.swift— Version comparison with remote config, hard-block vs soft-prompt UI, App Store redirectStateRestorationManager.swift—@MainActor @Observablestate restoration withNavigationPathpersistence, debounced save, restore-behavior policies
Cross-References
macos-development— macOS architecture patterns, SwiftData setup, AppKit bridgingswift-concurrency— async/await in app delegate methods and background tasksswift-networking— background transfer configuration for downloadsapp-development-workflow— lifecycle Phase 0-1 contextswift-actor-persistence— actor-based repositories for implementingsave()on.backgroundscene phaseswiftui-26-api—UIHostingSceneDelegatefor bridging UIKit scene lifecycle to SwiftUI
Weekly Installs
2
Repository
kmshdev/claude-…-toolkitFirst Seen
Today
Security Audits
Installed on
mcpjam2
claude-code2
replit2
junie2
windsurf2
zencoder2