Dependency Injection — довольно популярный паттерн, позволяющий гибко конфигурировать систему и правильно выстраивать зависимости компонентов этой системы друг от друга. Благодаря типизации, Swift позволяет использовать удобные фреймворки с помощью которых можно очень коротко описать граф зависимостей. Сегодня я хочу немного рассказать об одном из таких фреймворков — DITranquillity.
В данном туториале будут рассмотрены следующие возможности библиотеки:
- Регистрация типов
 - Внедрение с помощью инициализатора
 - Внедрение в переменную
 - Циклические зависимости компонентов
 - Использование библиотеки с 
UIStoryboard 
Описание компонентов
Приложение будет состоять из следующих основных компонентов: ViewController, Router, Presenter, Networking — это довольно общие компоненты в любом iOS приложении.

ViewController и Router будут внедряться друг в друга циклически.
Подготовка
Для начала создадим Single View Application в XCode, добавим DITranquillity с помощью CocoaPods. Создадим необходимую иерархию файлов, затем добавим на Main.storyboard второй контроллер и соединим его с помощью StoryboardSegue. В итоге должна получиться следующая структура файлов:

Создадим зависимости в классах следующим образом:
protocol Presenter: class {
    func getCounter(completion: @escaping (Int) -> Void)
}
class MyPresenter: Presenter {
    private let networking: Networking
    init(networking: Networking) {
        self.networking = networking
    }
    func getCounter(completion: @escaping (Int) -> Void) {
        // Implementation
    }
}protocol Networking: class {
    func fetchData(completion: @escaping (Result<Int, Error>) -> Void)
}
class MyNetworking: Networking {
    func fetchData(completion: @escaping (Result<Int, Error>) -> Void) {
        // Implementation
    }
}protocol Router: class {
    func presentNewController()
}
class MyRouter: Router {
    unowned let viewController: ViewController
    init(viewController: ViewController) {
        self.viewController = viewController
    }
    func presentNewController() {
        // Implementation
    }
}class ViewController: UIViewController {
    var presenter: Presenter!
    var router: Router!
}Ограничения
В отличие от других классов, ViewController создается не нами, а библиотекой UIKit внутри реализации UIStoryboard.instantiateViewController, поэтому, пользуясь сторибордом, мы не можем внедрять зависимости в наследников UIViewController с помощью инициализатора. Так же дела обстоят и с наследниками UIView и UITableViewCell.
Заметьте, что во все классы внедряются объекты, скрытые за протоколами. В этом одна из основных задачь внедрения зависимостей — сделать зависимости не от реализаций, а от интерфейсов. Это поможет в будущем предоставить разные реализации протоколов для переиспользования или тестирования компонентов.
Внедрение зависимостей
После того, как все компоненты системы созданы, приступим к связи объектов между собой. В DITranquillity отправной точной является DIContainer, который добавляет в себя регистрации с помощью метода container.register(...). Для разделения зависимостей на части используются DIFramework и DIPart, которые необходимо реализовать. Для удобства создадим только один класс ApplicationDependency, который будет реализовывать DIFramework и будет служить местом регистраций всех зависимостей. Интерфейс DIFramework обязывает реализовать только один метод — load(container:).
class ApplicationDependency: DIFramework {
    static func load(container: DIContainer) {
        // registrations will be placed here
    }
}Начнём с самой простой регистрации, у которой нет своих зависимостей — MyNetworking
container.register(MyNetworking.init)Данная регистрация использует внедрение через инициализатор. Несмотря на то, что у самого компонента нет зависимостей, ининциализатор необходимо предоставить, чтобы дать понять библиотеке, как создавать компонент.
Аналогичным образом зарегистрируем MyPresenter и MyRouter.
container.register1(MyPresenter.init)
container.register1(MyRouter.init)Note: Заметьте, что используется не register, а register1. К сожалению, так необходимо указывать, если объект имеет в инициализаторе одну и только одну зависимость. То есть, если зависимостей 0 или две и больше, необходимо использовать просто register. Данное ограничение является багом Swift версии 4.0 и больше.
Пришла пора регистрировать наш ViewController. Он внедряет объекты не через инициализатор, а напрямую в переменную, поэтому описание регистрации получится чуть больше.
container.register(ViewController.self)
    .injection(cycle: true, \.router)
    .injection(\.presenter)Синтаксис вида \.presenter является SwiftKeyPath, благодаря которому можно лаконично внедрить зависимость. Так как Router и ViewController циклически зависят друг от друга, необходимо явно это указать с помощью cycle: true. Библиотека и сама может разрешить эти зависимости без явного указания, но данное требование было введено, чтобы человек, читающий граф, сразу понимал, что в цепочке зависимостей есть циклы. Так же обратите внимание, что используется НЕ ViewController.init, но ViewController.self. Об этом писалось выше в разделе Ограничения.
Также необходимо зарегистрировать UIStoryboard с помощью специального метода.
container.registerStoryboard(name: "Main")Теперь у нас описан весь граф зависимостей для одного экрана. Но доступа к этому графу пока нет. Необходимо создать DIContainer, позволяющий получить доступ к объектам в нём.
static let container: DIContainer = {
    let container = DIContainer() // 1
    container.append(framework: ApplicationDependency.self) // 2
    assert(container.validate(checkGraphCycles: true)) // 3
    return container
}()- Инициализируем контейнер
 - Добавляем описание графа к нему
 - Проверяем, что мы всё сделали правильно. Если допущена ошибка, приложение упадёт не во время резолва зависимостей, а сразу при создании графа
 
Затем необходимо сделать контейнер отправной точкой старта приложения. Для этого в AppDelegate реализовываем метод didFinishLaunchingWithOptions вместо указания Main.storyboard как точко запуска в настройках проекта.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)
    let storyboard: UIStoryboard = ApplicationDependency.container.resolve()
    window?.rootViewController = storyboard.instantiateInitialViewController()
    window?.makeKeyAndVisible()
    return true
}Запуск
При первом запуске произойдёт падение и валидация не пройдёт по следующим причинам:
- Контейнер не найдёт типы 
Router,Presenter,Networking, потому что мы зарегистрировали только объекты. Если мы хотим дать доступ не к реализациям, а к интерфейсам, необходимо явно указать интерфейсы - Контейнер не понимает, как ему разрешить циклическую зависимость, потому что необходимо явно указать, какие объекты при резолве графа не должны каждый раз пересоздаваться
 
Исправить первую ошибку просто — есть специальный метод, позволяющий указать, под какими протоколами доступен метод в контейнере.
container.register(MyNetworking.init)
    .as(check: Networking.self) {$0}Описывая регистрацию так, мы говорим: объект MyNetworking доступен по протоколу Networking. Так нужно сделать для всех объектов, спрятанных под протоколами. {$0} добавляем для правильной проверки типов компилятором.
Со второй ошибкой чуть сложнее. Необходимо использовать так называемые scope, которые описывают, как часто создаётся и сколько живеёт объект. Для каждой регистрации, участвующей в циклической зависимости, необходимо указать scope равный objectGraph. Это даст понять контейнеру, что во время резолва необходимо переиспользовать одни и те же созданные объекты, а не создавать каждый раз заного. Таким образом, получится:
container.register(ViewController.self)
    .injection(cycle: true, \.router)
    .injection(\.presenter)
    .lifetime(.objectGraph)
container.register1(MyRouter.init)
    .as(check: Router.self) {$0}
    .lifetime(.objectGraph)После повторного запуска контейнер успешно проходит валидацию и откроется наш ViewController с созданными зависимостями. Можете поставить брейкпоинт во viewDidLoad и удостовериться.
Переход между экранами
Далее создадим два небольших класса SecondViewController и SecondPresenter, добавим SecondViewController на сториборд и создадим между ними Segue с идентификатором "RouteToSecond", позволяющий открыть второй контроллер из первого.
Добавим в наш ApplicationDependency ещё две регистрации для каждого из новых классов:
container.register(SecondViewController.self)
    .injection(\.secondPresenter)
container.register(SecondPresenter.init)Указывать .as нет необходимости, потому что мы не прятали SecondPresenter за протоколом, а пользуемся непосредственно реализацией. Затем в методе viewDidAppear первого контроллера вызываем performSegue(withIdentifier: "RouteToSecond", sender: self), запускаем, открывается второй контроллер, в котором котором должна быть проставлена зависимость secondPresenter. Как видно, контейнер увидел создание второго контроллера из UIStoryboard и успешно проставил зависимости.
Заключение
Данная библиотека позволяет удобно работать с циклическими зависимостями, сторибордом и полностью пользуется автовыводом типов в Swift, что даеёт очень короткий и гибкий синтаксис описания графа зависимостей.
Ссылки
Полный пример кода в библиотеке на github
DITranquillity на github