Files
obsidian-vault/work/wiki/apple-browsers/app-lifecycle-state-machine.md
T

577 lines
14 KiB
Markdown

---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/app-lifecycle-state-machine.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# App Lifecycle State Machine Architecture
## Overview
The DuckDuckGo browser has moved away from traditional AppDelegate-based lifecycle handling to a **state machine architecture**. While AppDelegate still exists, it has been significantly thinned out and now delegates responsibility to a structured state machine.
This approach ensures that lifecycle handling is **predictable, organized, and easy to maintain**.
## Architecture Components
### Three Core States
The architecture revolves around a state machine with three major states:
#### 1. **Launching** (Transient State)
- **Associated with**: `application(_:didFinishLaunchingWithOptions:)`
- **File**: `Launching.swift`
- **Purpose**: App's initial setup and dependency configuration
- **Responsibilities**:
- Initialize all services and objects
- Configure dependencies
- Prepare UI components
- Create `MainViewController` and set as `rootViewController`
#### 2. **Foreground** (Permanent State)
- **Associated with**: `applicationDidBecomeActive(_:)`
- **File**: `Foreground.swift`
- **Purpose**: App is fully interactive and user can engage with UI
- **Responsibilities**:
- Resume suspended work
- Handle user interactions
- Manage active UI state
#### 3. **Background** (Permanent State)
- **Associated with**: `applicationDidEnterBackground(_:)`
- **File**: `Background.swift`
- **Purpose**: App is not active and UI is not visible
- **Responsibilities**:
- Suspend ongoing work that doesn't need background execution
- Prepare for potential termination
- Handle background tasks
## State Machine Methods
### Core Transition Methods
All states implement specific methods for handling transitions:
#### `onTransition()`
- **When**: Called whenever the app enters that state from another state
- **Purpose**: Setup or cleanup during state transitions
- **Available in**: Foreground, Background
#### `willLeave()`
- **When**: Called before transitioning away from current state
- **Purpose**: Prepare for potential state change
- **Note**: Transition may be cancelled, in which case `didReturn()` is called
- **Available in**: Foreground, Background
#### `didReturn()`
- **When**: Called after successful transition to destination state OR when transition is cancelled
- **Purpose**: Finalize state entry or handle cancelled transition
- **Available in**: Foreground, Background
## Common Lifecycle Scenarios
### Cold App Start
```swift
// Flow: Launching → Foreground
1. Launching.init() // Initial setup
2. Foreground.onTransition() // Enter foreground
3. Foreground.didReturn() // Finalize foreground entry
```
### App Backgrounding
```swift
// Flow: Foreground → Background
1. Foreground.willLeave() // Prepare to leave foreground
2. Background.onTransition() // Enter background
3. Background.didReturn() // Finalize background entry
```
### App Foregrounding
```swift
// Flow: Background → Foreground
1. Background.willLeave() // Prepare to leave background
2. Foreground.onTransition() // Enter foreground
3. Foreground.didReturn() // Finalize foreground entry
```
### Interrupted Foreground (Alert/App Switcher)
```swift
// User receives alert but dismisses it
1. Foreground.willLeave() // Attempt to leave
2. Foreground.didReturn() // Cancelled - stay in foreground
// User opens App Switcher
1. Foreground.willLeave() // Attempt to leave
// Two possible outcomes:
// A. User returns directly:
2. Foreground.didReturn() // Return to foreground
// B. User switches to another app:
2. Background.onTransition() // Actually transition to background
3. Background.didReturn() // Finalize background entry
```
## Special iOS 18+ Scenarios
### Face ID Authentication on Cold Start
#### Successful Authentication
```swift
1. Launching.init()
2. Foreground.onTransition()
3. Foreground.didReturn()
```
#### Failed Authentication
```swift
1. Launching.init()
2. Background.onTransition() // Goes to background on auth failure
3. Background.didReturn()
```
### DuckDuckGo Face ID Lock
#### Cold Start with DDG Face ID
```swift
1. Launching.init()
2. Foreground.onTransition()
3. Foreground.didReturn()
4. Foreground.willLeave() // DDG auth triggers
5. Foreground.didReturn() // User passes auth
```
### Critical Setup Failure
```swift
1. Launching.init() throws // Setup fails (e.g., disk space)
2. Terminating.init() // App terminates
```
## Code Placement Patterns
### ⚙️ One-time Setup → `AppConfiguration`
**Location**: Inside `Launching.swift`
For setup that happens once and doesn't need ongoing lifecycle management:
```swift
class AppConfiguration {
func start() {
// Basic setup that doesn't require dependencies
setupGlobalUserAgent()
configureLogging()
}
func finalize() {
// Setup that requires access to services or MainCoordinator
configureWithDependencies()
}
}
```
**Use Cases**:
- Setting global user agents
- Initial configuration
- One-time system setup
### 🔄 Lifecycle-Reactive Logic → `Service`
For code that needs to react to app lifecycle events:
```swift
class MyLifecycleService {
func resumeWork() {
// Called from Foreground.onTransition() or didReturn()
}
func suspendWork() {
// Called from Background.onTransition() or Foreground.willLeave()
}
}
// In Launching.swift
let myService = MyLifecycleService()
services.myService = myService // Store in services for lifecycle access
```
**Service Patterns**:
- **Initialize**: In `Launching.init()`
- **Resume work**: In `Foreground` methods
- **Suspend work**: In `Background` methods
- **Assign to services**: Make available to other states
**Use Cases**:
- Network managers
- Timer services
- Data synchronization
- Background task management
### 🖼️ UI-Related Logic → `MainCoordinator`
**Location**: MainCoordinator initialization and management
For logic that involves creating or modifying the main view:
```swift
class MainCoordinator {
func setupMainViewController() {
// UI setup and configuration
}
func handleDeepLink(_ url: URL) {
// Navigation and UI state changes
}
}
```
**Use Cases**:
- View controller creation
- Navigation management
- UI state configuration
- Deep link handling
## Practical Examples
### 📊 Example 1: Pixel Analytics Service
**Requirement**: Send "Hello" pixel on foreground, "Goodbye" pixel on background
```swift
// 1. Create Service
class PixelService {
func sendHelloPixel() {
// Send hello pixel
}
func sendGoodbyePixel() {
// Send goodbye pixel
}
}
// 2. Initialize in Launching
class Launching {
func init() {
let pixelService = PixelService()
services.pixelService = pixelService
}
}
// 3. Use in Foreground
class Foreground {
func onTransition() {
services.pixelService.sendHelloPixel()
}
}
// 4. Use in Background
class Background {
func onTransition() {
services.pixelService.sendGoodbyePixel()
}
}
```
### ⏱️ Example 2: Session Timer Service
**Requirement**: Track session time, pause on interruptions, resume on return
```swift
class SessionTimeService {
private var timer: Timer?
func startTimer() {
// Start session timing
}
func pauseTimer() {
// Pause session timing
}
func resumeTimer() {
// Resume session timing
}
}
// Launching
class Launching {
func init() {
let sessionService = SessionTimeService()
services.sessionService = sessionService
}
}
// Foreground - Handle interruptions
class Foreground {
func didReturn() {
// Start/resume timer when entering or returning to foreground
services.sessionService.resumeTimer()
}
func willLeave() {
// Pause timer when potentially leaving foreground
services.sessionService.pauseTimer()
}
}
// Background
class Background {
func onTransition() {
// Timer already paused by Foreground.willLeave()
}
}
```
### 🧹 Example 3: Auto-Clear Data Service
**Requirement**: Clear data immediately on app wake to avoid UI glitches
```swift
class AutoClearService {
func startDataClearing() async {
// Clear user data
}
func waitForCompletion() async {
// Wait for clearing to complete
}
}
// Launching - Start clearing immediately
class Launching {
func init() {
let autoClearService = AutoClearService()
services.autoClearService = autoClearService
// Start clearing immediately on cold start
Task {
await autoClearService.startDataClearing()
}
}
}
// Foreground - Wait for completion before proceeding
class Foreground {
func onTransition() async {
// Wait for data clearing before loading URLs or handling deep links
await services.autoClearService.waitForCompletion()
handlePendingDeepLinks()
}
}
// Background - Start clearing before transitioning to foreground
class Background {
func willLeave() {
// Start clearing early to be ready for foreground transition
Task {
await services.autoClearService.startDataClearing()
}
}
func didReturn() {
// If transition was cancelled, clearing is still beneficial
// No action needed as clearing is irreversible
}
}
```
## State Context and Services
### Service Management
```swift
// Services are stored in StateContext for cross-state access
class StateContext {
var pixelService: PixelService!
var sessionService: SessionTimeService!
var autoClearService: AutoClearService!
// ... other services
}
// Access pattern in states
class Foreground {
func onTransition() {
services.pixelService.sendHelloPixel()
services.sessionService.resumeTimer()
}
}
```
### Service Lifecycle Best Practices
```swift
// ✅ CORRECT: Service with proper lifecycle management
class MyService {
private var isActive = false
func activate() {
guard !isActive else { return }
isActive = true
startWork()
}
func deactivate() {
guard isActive else { return }
isActive = false
stopWork()
}
private func startWork() {
// Begin service operations
}
private func stopWork() {
// Clean up service operations
}
}
// Usage in states
class Foreground {
func didReturn() {
services.myService.activate()
}
func willLeave() {
services.myService.deactivate()
}
}
```
## Decision Tree: Where Should My Code Go?
```
📋 What type of code are you adding?
├── 🔧 One-time setup that doesn't need lifecycle management?
│ └── ➡️ AppConfiguration (in Launching.swift)
│ ├── start() for basic setup
│ └── finalize() for dependency-requiring setup
├── 🔄 Logic that reacts to app state changes?
│ └── ➡️ Create a Service
│ ├── Initialize in Launching.init()
│ ├── Store in services for cross-state access
│ ├── Resume work in Foreground methods
│ └── Suspend work in Background methods
├── 🖼️ UI setup or view management?
│ └── ➡️ MainCoordinator
│ ├── View controller creation
│ ├── Navigation setup
│ └── Deep link handling
└── 🤔 Something else?
└── ➡️ Let's discuss through tech design
```
## Best Practices
### ✅ DO
```swift
// Store services for cross-state access
services.myService = MyService()
// Use proper lifecycle methods
func didReturn() {
resumeWork()
}
func willLeave() {
pauseWork()
}
// Handle state transitions gracefully
func onTransition() {
await waitForCriticalWork()
proceedWithStateLogic()
}
```
### ❌ DON'T
```swift
// Don't bypass the state machine
AppDelegate.shared.doSomething() // ❌
// Don't create services without storing them
let service = MyService() // ❌ Will be deallocated
// Don't ignore willLeave/didReturn patterns
func onTransition() {
// Only using onTransition misses important interrupt scenarios
}
// Don't block UI with long operations
func onTransition() {
performLongRunningTask() // ❌ Should be async
}
```
### 🔒 Memory Management
```swift
// Services are retained by StateContext
class StateContext {
var services: [String: AnyObject] = [:]
func addService<T: AnyObject>(_ service: T, for key: String) {
services[key] = service
}
}
// Clean up resources in state transitions
class MyService {
func cleanup() {
// Release resources, cancel operations
}
}
```
## Debugging and Monitoring
### State Transition Logging
```swift
class Foreground {
func onTransition() {
Logger.lifecycle.info("Entering Foreground state")
// State logic
}
func willLeave() {
Logger.lifecycle.info("Will leave Foreground state")
// Cleanup logic
}
func didReturn() {
Logger.lifecycle.info("Returned to Foreground state")
// Resume logic
}
}
```
### Performance Monitoring
```swift
class Launching {
func init() {
let startTime = CFAbsoluteTimeGetCurrent()
// Initialization logic
let duration = CFAbsoluteTimeGetCurrent() - startTime
Logger.performance.info("Launching completed in \(duration)s")
}
}
```
---
This state machine architecture provides a robust, maintainable approach to app lifecycle management that scales with the complexity of the DuckDuckGo browser while maintaining clear separation of concerns.