← Назад к вопросам

Какой DI используешь в контексте SwiftUI?

1.6 Junior🔥 221 комментариев
#SwiftUI

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Мой подход к Dependency Injection в SwiftUI

В контексте SwiftUI я использую комбинированный подход, который сочетает несколько паттернов DI, адаптированных под реактивную природу фреймворка. Основные инструменты:

1. Environment и EnvironmentObject

Это нативные механизмы SwiftUI, которые я считаю фундаментальными для передачи зависимостей через иерархию представлений:

// Определение зависимости
protocol AnalyticsService {
    func trackEvent(_ event: String)
}

class ProductionAnalytics: AnalyticsService, ObservableObject {
    func trackEvent(_ event: String) {
        // Реализация
    }
}

// Вставка в корне представления
struct AppRoot: View {
    @StateObject private var analytics = ProductionAnalytics()
    
    var body: some View {
        ContentView()
            .environmentObject(analytics)
            .environment(\.analyticsService, analytics)
    }
}

// Кастомный EnvironmentKey
struct AnalyticsServiceKey: EnvironmentKey {
    static let defaultValue: AnalyticsService? = nil
}

extension EnvironmentValues {
    var analyticsService: AnalyticsService? {
        get { self[AnalyticsServiceKey.self] }
        set { self[AnalyticsServiceKey.self] = newValue }
    }
}

// Использование в дочернем View
struct ProductView: View {
    @Environment(\.analyticsService) private var analytics
    
    var body: some View {
        Button("Купить") {
            analytics?.trackEvent("purchase_initiated")
        }
    }
}

2. Property Injection с инициализаторами

Для явного контроля зависимостей и улучшения тестируемости:

class ProductViewModel: ObservableObject {
    private let paymentProcessor: PaymentProcessing
    private let analytics: AnalyticsService
    
    // Явный инициализатор с зависимостями
    init(paymentProcessor: PaymentProcessing, 
         analytics: AnalyticsService) {
        self.paymentProcessor = paymentProcessor
        self.analytics = analytics
    }
    
    func purchaseProduct(_ product: Product) {
        analytics.trackEvent("purchase_started")
        // Логика покупки
    }
}

3. Контейнер зависимостей

Для сложных приложений я реализую легковесный DI-контейнер:

protocol DIContainerProtocol {
    func register<Service>(_ type: Service.Type, factory: @escaping () -> Service)
    func resolve<Service>(_ type: Service.Type) -> Service?
}

class DIContainer: DIContainerProtocol {
    private var services: [String: () -> Any] = [:]
    
    func register<Service>(_ type: Service.Type, factory: @escaping () -> Service) {
        services["\(type)"] = factory
    }
    
    func resolve<Service>(_ type: Service.Type) -> Service? {
        services["\(type)"]?() as? Service
    }
}

// Конфигурация в точке входа
@main
struct MyApp: App {
    private let container = DIContainer()
    
    init() {
        setupDependencies()
    }
    
    private func setupDependencies() {
        container.register(AnalyticsService.self) {
            ProductionAnalytics()
        }
        container.register(NetworkService.self) {
            NetworkManager(config: .production)
        }
    }
    
    var body: some Scene {
        WindowGroup {
            AppRootView()
                .environment(\.diContainer, container)
        }
    }
}

4. Factory и Assambly подход

Для модульности и разделения ответственности:

// Модуль аутентификации
struct AuthAssembly {
    func makeLoginView() -> some View {
        let service = AuthService()
        let viewModel = LoginViewModel(authService: service)
        return LoginView(viewModel: viewModel)
    }
}

// Использование
struct AppRouter: View {
    private let authAssembly = AuthAssembly()
    
    var body: some View {
        NavigationView {
            authAssembly.makeLoginView()
        }
    }
}

Ключевые принципы моего подхода:

Стратификация зависимостей:

  • EnvironmentObject для глобальных состояний (UserSession, AppSettings)
  • Environment для сервисов (Analytics, Networking)
  • Property Injection для ViewModel и бизнес-логики
  • Контейнер для синглтонов и сложных графов зависимостей

Тестируемость:

// Тестовый пример
func testProductViewModel() {
    let mockAnalytics = MockAnalyticsService()
    let mockPayment = MockPaymentProcessor()
    
    let viewModel = ProductViewModel(
        paymentProcessor: mockPayment,
        analytics: mockAnalytics
    )
    
    // Проверка взаимодействий
    viewModel.purchaseProduct(testProduct)
    XCTAssertTrue(mockAnalytics.didTrackEvent)
}

Модульность и изоляция:

  • Каждый фичер имеет собственные зависимости
  • Общие сервисы инжектятся через протоколы
  • Четкое разделение на слои (Presentation, Domain, Data)

Почему такой комбинированный подход?

  1. Использование нативных возможностей SwiftUI (Environment) уменьшает boilerplate код
  2. Property Injection через инициализаторы обеспечивает явность и тестируемость
  3. Легковесный контейнер решает проблему создания сложных объектов
  4. Протокол-ориентированный дизайн позволяет легко подменять реализации

Этот подход балансирует между практичностью SwiftUI и строгостью классических DI-подходов, обеспечивая чистую архитектуру без излишней сложности.

Какой DI используешь в контексте SwiftUI? | PrepBro