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

448 lines
11 KiB
Markdown

---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/ios-architecture.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# iOS DuckDuckGo Browser Architecture Rules
## Dependency Injection and AppDependencies
### Use AppDependencyProvider Pattern
ALWAYS use the centralized dependency provider for all service dependencies:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ 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:
```swift
// ✅ CORRECT - Task usage in ViewModels
final class ViewModel: ObservableObject {
func performAction() {
Task {
await loadData()
}
}
}
```
## Singleton Pattern Guidelines
### Avoid Singletons - Use Dependency Injection
```swift
// ❌ 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:
```swift
// ✅ 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
```swift
// ✅ 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:
```swift
// ✅ 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
```swift
// ✅ CORRECT - Lazy property initialization
final class DataManager {
private lazy var expensiveResource: ExpensiveResource = {
return ExpensiveResource()
}()
}
```
### Background Processing
```swift
// ✅ 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:
```swift
// ✅ 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)
}
}
```