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