577 lines
14 KiB
Markdown
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.
|