Swinject для Swift: полное руководство с примерами

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

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

Начать курс

Что такое Swinject и зачем он нужен?

Swinject — это легковесная библиотека для внедрения зависимостей (Dependency Injection, DI) на языке Swift. Она предоставляет простой и декларативный способ управления зависимостями между объектами, что делает код более модульным, тестируемым и поддерживаемым.

В традиционном подходе объекты сами создают свои зависимости, что приводит к жесткой связанности кода. Например, класс UserService может напрямую создавать экземпляр NetworkClient. Если вы захотите заменить NetworkClient на мок-объект для тестирования, вам придется менять код UserService. Swinject решает эту проблему, вынося создание и связывание объектов в отдельный контейнер.

Основные преимущества Swinject:

  • Снижение связанности: Классы не знают, как создаются их зависимости.
  • Улучшение тестируемости: Легко подменять реальные зависимости на моки и стабы.
  • Упрощение конфигурации: Вся конфигурация зависимостей собирается в одном месте.
  • Поддержка жизненного цикла: Управление временем жизни объектов (синглтон, новый экземпляр и т.д.).
  • Чистая архитектура: Способствует следованию принципам SOLID, особенно Dependency Inversion Principle.

Swinject особенно полезен в больших проектах, где количество классов и их зависимостей растет, и ручное управление ими становится сложным и подверженным ошибкам.

Установка Swinject

Swinject устанавливается через Swift Package Manager (SPM) или CocoaPods. Мы рассмотрим оба способа.

Установка через Swift Package Manager (рекомендуется)

В Xcode перейдите в File → Add Packages.... В строку поиска введите URL репозитория: https://github.com/Swinject/Swinject.git. Выберите нужную версию (обычно последнюю стабильную) и нажмите Add Package.

Альтернативно, вы можете добавить зависимость в ваш Package.swift:

// swift-tools-version:5.7
import PackageDescription

let package = Package(
    name: "YourProject",
    dependencies: [
        .package(url: "https://github.com/Swinject/Swinject.git", from: "2.8.0")
    ],
    targets: [
        .target(
            name: "YourTarget",
            dependencies: ["Swinject"])
    ]
)

Установка через CocoaPods

Добавьте в ваш Podfile:

pod 'Swinject', '~> 2.8'

Затем выполните команду pod install.

Примечание: В отличие от Python (pip) или Node.js (npm), в Swift нет единого менеджера пакетов, но SPM является стандартным и встроенным в Xcode.

Быстрый старт с примером кода

Рассмотрим простейший пример: у нас есть протокол LoggerProtocol и его реализация ConsoleLogger. Мы хотим, чтобы класс UserService получал логгер через внедрение зависимостей.

import Swinject

// 1. Определяем протокол
protocol LoggerProtocol {
    func log(message: String)
}

// 2. Реализация протокола
class ConsoleLogger: LoggerProtocol {
    func log(message: String) {
        print("LOG: \(message)")
    }
}

// 3. Класс, который использует зависимость
class UserService {
    private let logger: LoggerProtocol

    // Внедрение через инициализатор
    init(logger: LoggerProtocol) {
        self.logger = logger
    }

    func createUser(name: String) {
        logger.log(message: "Creating user: \(name)")
        // Логика создания пользователя
    }
}

// 4. Настройка контейнера
let container = Container()
container.register(LoggerProtocol.self) { _ in ConsoleLogger() }
container.register(UserService.self) { r in
    UserService(logger: r.resolve(LoggerProtocol.self)!)
}

// 5. Использование
let userService = container.resolve(UserService.self)!
userService.createUser(name: "Alice")
// Вывод: LOG: Creating user: Alice

В этом примере мы создали контейнер, зарегистрировали в нем протоколы и их реализации, а затем разрешили зависимость. Обратите внимание, что UserService не знает о существовании ConsoleLogger — он работает только с протоколом.

Основные методы и функции Swinject

Ниже приведен подробный разбор ключевых методов библиотеки.

1. register(_:factory:)

Что делает: Регистрирует сервис (тип) в контейнере с фабрикой, которая создает экземпляр этого сервиса.

Параметры:

  • serviceType: Тип сервиса (обычно протокол или класс).
  • name (опционально): Имя регистрации для различения нескольких реализаций одного типа.
  • factory: Замыкание, которое принимает Resolver и возвращает экземпляр сервиса.

Рабочий пример:

container.register(LoggerProtocol.self) { _ in ConsoleLogger() }
// Регистрация с именем
container.register(LoggerProtocol.self, name: "file") { _ in FileLogger() }

2. resolve(_:name:)

Что делает: Разрешает зависимость — возвращает экземпляр зарегистрированного сервиса.

Параметры:

  • serviceType: Тип сервиса.
  • name (опционально): Имя регистрации.

Рабочий пример:

let logger = container.resolve(LoggerProtocol.self)!
let fileLogger = container.resolve(LoggerProtocol.self, name: "file")!

3. register(_:factory:inObjectScope:)

Что делает: Регистрирует сервис с указанием области видимости (жизненного цикла).

Параметры:

  • serviceType: Тип сервиса.
  • factory: Фабрика.
  • inObjectScope: Область видимости (например, .container, .graph, .transient).

Рабочий пример:

// Синглтон на уровне контейнера
container.register(NetworkClient.self) { _ in NetworkClient() }
    .inObjectScope(.container)

// Новый экземпляр при каждом resolve
container.register(NetworkClient.self) { _ in NetworkClient() }
    .inObjectScope(.transient)

4. inObjectScope(_:)

Что делает: Метод, вызываемый на регистрации для установки области видимости.

Параметры:

  • scope: Объект типа ObjectScope.

Рабочий пример:

container.register(APIService.self) { _ in APIService() }
    .inObjectScope(.container) // Синглтон

5. register(_:factory:name:) с именем

Что делает: Позволяет регистрировать несколько реализаций одного типа под разными именами.

Параметры: Аналогичны register, но добавляется параметр name.

Рабочий пример:

protocol Cache {
    func save(key: String, value: Any)
}

class MemoryCache: Cache { ... }
class DiskCache: Cache { ... }

container.register(Cache.self, name: "memory") { _ in MemoryCache() }
container.register(Cache.self, name: "disk") { _ in DiskCache() }

let memoryCache = container.resolve(Cache.self, name: "memory")!

6. resolve(_:name:argument:)

Что делает: Разрешает зависимость, передавая дополнительный аргумент в фабрику.

Параметры:

  • serviceType: Тип сервиса.
  • name (опционально): Имя.
  • argument: Аргумент, который будет передан в фабрику.

Рабочий пример:

container.register(LoggerProtocol.self) { (_, prefix: String) in
    return PrefixedLogger(prefix: prefix)
}

let logger = container.resolve(LoggerProtocol.self, argument: "APP")!

7. register(_:factory:inObjectScope:name:) — полная форма

Что делает: Комбинирует все параметры регистрации.

Параметры: Все вышеперечисленные.

Рабочий пример:

container.register(DataService.self, name: "production") { r in
    let network = r.resolve(NetworkClient.self)!
    return DataService(networkClient: network)
}.inObjectScope(.container)

8. Container(parent:)

Что делает: Создает дочерний контейнер, который наследует регистрации от родительского.

Параметры:

  • parent: Родительский контейнер.

Рабочий пример:

let parentContainer = Container()
parentContainer.register(LoggerProtocol.self) { _ in ConsoleLogger() }

let childContainer = Container(parent: parentContainer)
// childContainer может разрешить LoggerProtocol, даже если он не зарегистрирован в нем напрямую
let logger = childContainer.resolve(LoggerProtocol.self)!

9. removeAll()

Что делает: Удаляет все регистрации из контейнера.

Параметры: Нет.

Рабочий пример:

container.removeAll()
// После этого container не сможет разрешить ни одну зависимость

10. remove(for:name:)

Что делает: Удаляет конкретную регистрацию по типу и имени.

Параметры:

  • serviceType: Тип сервиса.
  • name (опционально): Имя.

Рабочий пример:

container.remove(for: LoggerProtocol.self, name: "file")
// Теперь "file" логгер недоступен

Реальный практический пример использования

Рассмотрим более сложный пример: приложение для управления задачами (Task Manager). У нас есть сервис для работы с задачами, репозиторий для хранения данных и логгер.

import Swinject

// MARK: - Протоколы
protocol TaskRepositoryProtocol {
    func getAllTasks() -> [String]
    func addTask(_ task: String)
}

protocol TaskServiceProtocol {
    func fetchTasks() -> [String]
    func createTask(_ task: String)
}

protocol LoggerProtocol {
    func log(_ message: String)
}

// MARK: - Реализации
class InMemoryTaskRepository: TaskRepositoryProtocol {
    private var tasks: [String] = []

    func getAllTasks() -> [String] {
        return tasks
    }

    func addTask(_ task: String) {
        tasks.append(task)
    }
}

class TaskService: TaskServiceProtocol {
    private let repository: TaskRepositoryProtocol
    private let logger: LoggerProtocol

    init(repository: TaskRepositoryProtocol, logger: LoggerProtocol) {
        self.repository = repository
        self.logger = logger
    }

    func fetchTasks() -> [String] {
        logger.log("Fetching all tasks")
        return repository.getAllTasks()
    }

    func createTask(_ task: String) {
        logger.log                    

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