Dependency Injection контейнер для Swift (iOS/macOS): Swinject

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Swinject — Dependency Injection контейнер для Swift (iOS/macOS)

Dependency Injection (DI) — один из ключевых принципов SOLID, позволяющий создавать слабосвязанный, тестируемый и масштабируемый код. В мире Swift для iOS и macOS разработки одной из самых популярных библиотек, реализующих DI, является Swinject. Это легковесный, но мощный контейнер внедрения зависимостей, который упрощает управление объектами и их жизненным циклом.

В этой статье мы подробно разберём, как работает Swinject, как его установить и использовать в реальных проектах.

Что такое Dependency Injection и зачем нужен контейнер?

Внедрение зависимостей — это техника, при которой объект получает свои зависимости извне, а не создаёт их сам. Это повышает гибкость и упрощает тестирование. Контейнер DI (например, Swinject) автоматизирует процесс создания и связывания объектов, избавляя разработчика от написания boilerplate-кода.

Установка Swinject

Swinject можно установить с помощью Swift Package Manager, CocoaPods или Carthage. Рассмотрим самый современный способ — SPM:

// В Xcode: File → Swift Packages → Add Package Dependency
// URL: https://github.com/Swinject/Swinject.git
// Версия: от 2.8.0 и выше

Или добавьте в Package.swift:

dependencies: [
    .package(url: "https://github.com/Swinject/Swinject.git", from: "2.8.0")
]

Основные компоненты Swinject

  • Container — основной объект, в котором регистрируются и разрешаются зависимости.
  • Service — тип (протокол или класс), который мы хотим внедрить.
  • Registration — процесс связывания абстракции (протокола) с конкретной реализацией.
  • Scope — управляет жизненным циклом объекта (транзиентный, синглтон, и т.д.).

Базовый пример использования

Предположим, у нас есть протокол NetworkServiceProtocol и его реализация NetworkService:

import Swinject

protocol NetworkServiceProtocol {
    func fetchData() -> String
}

class NetworkService: NetworkServiceProtocol {
    func fetchData() -> String {
        return "Данные получены"
    }
}

// Создаём контейнер
let container = Container()

// Регистрируем зависимость
container.register(NetworkServiceProtocol.self) { _ in NetworkService() }

// Разрешаем (получаем) экземпляр
let service = container.resolve(NetworkServiceProtocol.self)!
print(service.fetchData()) // "Данные получены"

Внедрение зависимостей в ViewController (iOS)

Чаще всего Swinject используется для внедрения сервисов в контроллеры. Рассмотрим пример с вью-моделью:

import Swinject

// Протокол и реализация вью-модели
protocol UserViewModelProtocol {
    func getUsers() -> [String]
}

class UserViewModel: UserViewModelProtocol {
    private let networkService: NetworkServiceProtocol
    
    init(networkService: NetworkServiceProtocol) {
        self.networkService = networkService
    }
    
    func getUsers() -> [String] {
        // Используем networkService для загрузки
        return ["Alice", "Bob"]
    }
}

// Регистрация в контейнере
container.register(UserViewModelProtocol.self) { resolver in
    let network = resolver.resolve(NetworkServiceProtocol.self)!
    return UserViewModel(networkService: network)
}

// Использование в контроллере
class UsersViewController: UIViewController {
    var viewModel: UserViewModelProtocol?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel = container.resolve(UserViewModelProtocol.self)
        print(viewModel?.getUsers() ?? [])
    }
}

Жизненные циклы объектов (Scopes)

Swinject поддерживает несколько scopes:

  • .transient — каждый раз создаётся новый экземпляр (по умолчанию).
  • .container — синглтон в рамках одного контейнера.
  • .graph — экземпляр живёт только во время разрешения графа зависимостей.

Пример с синглтоном:

container.register(NetworkServiceProtocol.self) { _ in NetworkService() }
    .inObjectScope(.container) // теперь это синглтон

Именованные регистрации

Если нужно зарегистрировать несколько реализаций одного протокола, используйте имена:

container.register(NetworkServiceProtocol.self, name: "mock") { _ in MockNetworkService() }
container.register(NetworkServiceProtocol.self, name: "real") { _ in NetworkService() }

let mockService = container.resolve(NetworkServiceProtocol.self, name: "mock")

Assembly — модульная организация кода

Для больших проектов удобно группировать регистрации в отдельные классы — Assembly:

Рекомендаций