Files
obsidian-vault/work/wiki/apple-browsers/ios-architecture.md
T

11 KiB

source, confidence, namespace, last_synced, alwaysApply
source confidence namespace last_synced alwaysApply
~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/ios-architecture.mdc 0.9 work 2026-04-28 false

iOS DuckDuckGo Browser Architecture Rules

Dependency Injection and AppDependencies

Use AppDependencyProvider Pattern

ALWAYS use the centralized dependency provider for all service dependencies:

// ✅ CORRECT - Use the shared dependency provider
final class FeatureViewModel: ObservableObject {
    private let networkService: NetworkServiceProtocol
    
    init(dependencies: DependencyProvider = AppDependencyProvider.shared) {
        self.networkService = dependencies.networkService
    }
}

// ❌ INCORRECT - Direct singleton access
final class FeatureViewModel: ObservableObject {
    private let networkService = NetworkService.shared // Avoid singletons
}

Protocol-Based Dependencies

ALWAYS define protocols for dependencies to enable testing:

// ✅ CORRECT - Protocol abstraction
protocol FeatureServiceProtocol {
    func fetchData() async throws -> [Item]
}

final class FeatureService: FeatureServiceProtocol {
    // Implementation
}

// ❌ INCORRECT - Concrete dependency
final class ViewModel {
    private let service: FeatureService // Hard to test
}

AppSettings and Configuration

Use AppSettings Protocol

ALWAYS access settings through the AppSettings protocol:

// ✅ CORRECT - Protocol-based settings access
final class SettingsViewModel: ObservableObject {
    private let appSettings: AppSettings
    
    init(appSettings: AppSettings) {
        self.appSettings = appSettings
    }
    
    var isFeatureEnabled: Bool {
        get { appSettings.featureEnabled }
        set { appSettings.featureEnabled = newValue }
    }
}

// ❌ INCORRECT - Direct UserDefaults access
final class SettingsViewModel: ObservableObject {
    var isFeatureEnabled: Bool {
        get { UserDefaults.standard.bool(forKey: "feature_enabled") }
        set { UserDefaults.standard.set(newValue, forKey: "feature_enabled") }
    }
}

UserDefaults Property Wrapper

Use the established @UserDefaultsWrapper pattern for new settings:

// ✅ CORRECT - Property wrapper usage
extension AppUserDefaults {
    @UserDefaultsWrapper(key: .newFeatureEnabled, defaultValue: false)
    var newFeatureEnabled: Bool
}

// ❌ INCORRECT - Manual UserDefaults handling
extension AppUserDefaults {
    var newFeatureEnabled: Bool {
        get { userDefaults.bool(forKey: "new_feature_enabled") }
        set { userDefaults.set(newValue, forKey: "new_feature_enabled") }
    }
}

Navigation and Coordinators

Use MainCoordinator for App-Level Navigation

ALWAYS use MainCoordinator for deep links, URL handling, and app-level navigation:

// ✅ CORRECT - MainCoordinator usage
@MainActor
final class FeatureCoordinator {
    private weak var mainCoordinator: MainCoordinator?
    
    func handleFeatureAction() {
        mainCoordinator?.handleURL(featureURL)
    }
}

// ❌ INCORRECT - Direct navigation from ViewModels
final class FeatureViewModel: ObservableObject {
    func handleAction() {
        // Don't navigate directly from ViewModels
        navigationController?.pushViewController(detailVC, animated: true)
    }
}

URL Handling Pattern

Implement URLHandling protocol for custom URL schemes:

// ✅ CORRECT - URLHandling protocol implementation
extension FeatureCoordinator: URLHandling {
    func handleURL(_ url: URL) {
        guard url.scheme == "duckduckgo",
              url.host == "feature" else { return }
        
        presentFeature(with: url.queryParameters)
    }
    
    func shouldProcessDeepLink(_ url: URL) -> Bool {
        return url.scheme == "duckduckgo" && url.host == "feature"
    }
}

SwiftUI and Design System Integration

Use DesignResourcesKit Colors

ALWAYS use semantic colors from DesignResourcesKit:

// ✅ CORRECT - Semantic color usage
struct FeatureView: View {
    var body: some View {
        VStack {
            Text("Title")
                .foregroundColor(Color(designSystemColor: .textPrimary))
            
            Rectangle()
                .fill(Color(designSystemColor: .surface))
        }
        .background(Color(designSystemColor: .background))
    }
}

// ❌ INCORRECT - Hardcoded colors
struct FeatureView: View {
    var body: some View {
        VStack {
            Text("Title")
                .foregroundColor(.black) // Don't hardcode colors
            
            Rectangle()
                .fill(.gray) // Use semantic colors instead
        }
    }
}

Use DesignResourcesKit Icons

ALWAYS use icons from DesignResourcesKitIcons:

// ✅ CORRECT - Design system icons
struct IconButton: View {
    var body: some View {
        Button(action: action) {
            Image(uiImage: DesignSystemImages.Glyphs.Size16.add)
                .foregroundColor(Color(designSystemColor: .accent))
        }
    }
}

// ❌ INCORRECT - System icons or custom images
struct IconButton: View {
    var body: some View {
        Button(action: action) {
            Image(systemName: "plus") // Use design system icons
        }
    }
}

Theme Integration

Use Theme protocol for complex color requirements:

// ✅ CORRECT - Theme integration
struct ThemedView: View {
    @EnvironmentObject var themeManager: ThemeManager
    
    var body: some View {
        Rectangle()
            .fill(Color(themeManager.currentTheme.backgroundColor))
    }
}

MVVM Pattern Implementation

ObservableObject ViewModels

ALWAYS use ObservableObject for SwiftUI ViewModels:

// ✅ CORRECT - ObservableObject ViewModel
@MainActor
final class FeatureViewModel: ObservableObject {
    @Published private(set) var items: [Item] = []
    @Published private(set) var isLoading = false
    @Published private(set) var error: Error?
    
    private let service: FeatureServiceProtocol
    private var cancellables = Set<AnyCancellable>()
    
    init(service: FeatureServiceProtocol) {
        self.service = service
        setupBindings()
    }
    
    func loadData() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            items = try await service.fetchItems()
            error = nil
        } catch {
            self.error = error
        }
    }
    
    private func setupBindings() {
        // Setup Combine bindings
    }
}

Published Property Guidelines

  • Use @Published private(set) for read-only state
  • Use @Published for two-way bindings
  • Always mark ViewModels with @MainActor

Async/Await Patterns

MainActor Usage

ALWAYS use @MainActor for UI-related async operations:

// ✅ CORRECT - MainActor for UI updates
@MainActor
final class CoordinatorClass {
    func presentModal() async {
        let viewModel = try await createViewModel()
        let hostingController = UIHostingController(rootView: FeatureView(viewModel: viewModel))
        navigationController.present(hostingController, animated: true)
    }
}

// ❌ INCORRECT - UI updates without MainActor
final class CoordinatorClass {
    func presentModal() async {
        let viewModel = try await createViewModel()
        // This will crash - UI updates must be on main thread
        navigationController.present(hostingController, animated: true)
    }
}

Task Management

Use Task for async operations in ViewModels:

// ✅ CORRECT - Task usage in ViewModels
final class ViewModel: ObservableObject {
    func performAction() {
        Task {
            await loadData()
        }
    }
}

Singleton Pattern Guidelines

Avoid Singletons - Use Dependency Injection

// ❌ INCORRECT - Singleton pattern
final class FeatureManager {
    static let shared = FeatureManager()
    private init() {}
}

// ✅ CORRECT - Dependency injection
protocol FeatureManagerProtocol {
    func performAction()
}

final class FeatureManager: FeatureManagerProtocol {
    func performAction() {
        // Implementation
    }
}

// Register in AppDependencyProvider
extension AppDependencyProvider {
    var featureManager: FeatureManagerProtocol {
        return FeatureManager()
    }
}

Acceptable Singleton Usage

Only use singletons for truly global state that must be shared across the entire app:

// ✅ ACCEPTABLE - Global theme management
final class ThemeManager {
    static let shared = ThemeManager()
    private init() {}
}

// ✅ ACCEPTABLE - Content blocking state
final class ContentBlocking {
    static let shared = ContentBlocking()
    private init() {}
}

Error Handling

Use Result Types for Service Layer

// ✅ CORRECT - Result types in services
protocol NetworkServiceProtocol {
    func fetchData() async -> Result<Data, NetworkError>
}

// ✅ CORRECT - Async throws in ViewModels
final class ViewModel: ObservableObject {
    func loadData() async {
        do {
            let result = try await service.fetchData()
            // Handle success
        } catch {
            // Handle error
        }
    }
}

Custom Error Types

Define domain-specific error types:

// ✅ CORRECT - Domain-specific errors
enum FeatureError: LocalizedError {
    case networkUnavailable
    case invalidData
    case unauthorized
    
    var errorDescription: String? {
        switch self {
        case .networkUnavailable:
            return "Network connection unavailable"
        case .invalidData:
            return "Invalid data received"
        case .unauthorized:
            return "User not authorized"
        }
    }
}

Performance Optimization

Lazy Loading for Expensive Operations

// ✅ CORRECT - Lazy property initialization
final class DataManager {
    private lazy var expensiveResource: ExpensiveResource = {
        return ExpensiveResource()
    }()
}

Background Processing

// ✅ CORRECT - Background processing with main thread UI updates
final class ViewModel: ObservableObject {
    @Published var result: ProcessedData?
    
    func processData() {
        Task.detached(priority: .userInitiated) {
            let processed = await heavyProcessing()
            
            await MainActor.run {
                self.result = processed
            }
        }
    }
}

Testing Requirements

Testable Architecture

ALWAYS write testable code using dependency injection:

// ✅ CORRECT - Testable implementation
final class FeatureViewModel: ObservableObject {
    private let service: FeatureServiceProtocol
    
    init(service: FeatureServiceProtocol) {
        self.service = service
    }
}

// Test setup
final class FeatureViewModelTests: XCTestCase {
    func testLoadData() async {
        let mockService = MockFeatureService()
        let viewModel = FeatureViewModel(service: mockService)
        
        await viewModel.loadData()
        
        XCTAssertTrue(mockService.fetchDataCalled)
    }
}