swinject swift что это

MrDekk

© 2019. All rights reserved.

Внедрение зависимостей (dependency injection) в Swift 5.1

Внедрение зависимостей очень горячая тема в любой области разработки, где мы пишем что-то более сложное чем Hello, World. Однако несмотря на казалось бы изученный вдоль и поперек вопрос, вариантов его решения вы можете на просторах интернета найти великое множество. И в каждом месте оно подается как единственно правильное. И как же выбрать? Предлагаю в этой статье немножко рассмотреть подходы, их плюсы и минусы, немножко поиграться со Swift’ом вообще и попробовать его новые фичи в виде @PropertyWrapper’s.

Будем также считать что есть некая реализация протокола BooksProvider, например такая наивная

Теперь наша задача каким-то образом доставить экземпляр класса NaiveBooksProvider в BooksRenderer. Самый наивный подход такой, создать экземлляр класса прямо на месте:

Несмотря на то, что этот подход, каким бы наивным он не был, много где применяется, у него есть очевидные недостатки:

Нам надо что-то лучше. И много где предлагают хорошо известный паттер ServiceLocator. Если его применить, то выглядеть это будет примерно так:

Уже лучше, ответственность за выбор конкретного класса мы достали из BooksRenderer и наделили этой почетной обязанностью класс ServiceLocator. И мы даже можем сделать разные ServiceLocator’ы для основного приложения и для тестов, которые будут создавать разные BooksProvider’ы, однако:

Таким образом, можно сделать такой DI на базе initializer injection (оно лучше property injection, потому что компилятор в этом случае не даст вам озорничать, а с property injection легко забыть что-нибудь присвоить и грохнуться в рантайме):

Причем такой подход будет гарантировать вам проверку компилятором. И при этом про AppContainer будет знать только AppDelegate. Да, корневые зависимости в самом AppDelegate’е будут force unwrapped (что исть не хорошо, но лучше я не придумал), но эта вольность доступна только тут.

prootype зависимости в таком подходе можно оформить либо в виде фабрики

и потом внедрять это фабрику как singleton зависимость туда где нужно генерить prototype’ные, или в виде замыкания

и также ее внедрять как singleton зависимость.

Но я обещал немножко Swift 5.1 и @PropertyWrapper, их легко сделать так (пусть будет наш пример с ServiceLocator’ом, хотя его можно легко модифицировать):

И вуаля! Однако несмотря на всю прелесть такой магии, есть проблема в месте где творится магия (“do some magic”). Если вы вдруг забыли сделать register, то упс, вы получаете рантайм крэш. И сделать это красиво с compile time check непонятно как, так как регистрация динамическая.

Предлагаю подискутировать, оформляйте issues тут

Источник

iOS Dependency Injection Using Swinject

This blog will cover the following things:

Inversion Of Control (IOC)

In object-oriented programming it’s all about to remove the dependencies from your code.

Dependency Injection

In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. Dependency injection is one way to achieve the Inversion Of Control.

The idea behind this is to have a separate object create the required dependency, and pass it to the client.

Benefits

DI Container

Dependency Injection Container is an object that knows how to instantiate and configure objects. DI Container is a design pattern to implement Dependency injection. One benefit of using it to resolve Complex dependency

Swinject

Getting Started

We need to test loadData method of NetworkManager class. This method uses UrlSession to fetch data from the server and decode the response in the model. We can’t test this method until we have an internet connection which is not a very good practice. What if we somehow mock the server by injecting mock Urlsession which return mock data.

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

As shown in Figure 2 we created MockUrlSession and MockUrlSessionDataTask classes that act as a mock server which will return what we told. If you want to deep dive into the network testing strategies you can see this blog.

Come to the main point. We created urlSession property and injected it through its initializer. Now NetworkManager is not responsible for creating UrlSession since we inject it through its initializer what we achieved.

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

Why We Need Swinject

DI Container is a design pattern to implement Dependency injection. Most of the time, you don’t need a Dependency Injection Container to benefit from Dependency Injection. But when you need to manage a lot of different objects with a lot of dependencies, a Dependency Injection Container can be really helpful. Swinject is the implementation of DI Container in Swift.

As shown in Figure 4 we have a client A and B both need DataFetcher object. We are applying Dependency Injection by passing DataFetcher dependencies through its initializer. What can we observe by looking into this complex dependencies:

This is where DI container will be helpful. DI container manages the type dependencies of your system. First, you register the types that should be resolved, with their dependencies. Then you use the DI container to get instances of those types whose dependencies are then automatically resolved by the DI container. In Swinject, the Container class represents the DI container.

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

Before we implement the above code using Swinject we first need to have a good understanding of how Swinject works, it’s syntax and basic usage. You can also see this link.

Terms

As shown in Figure 5 we performed a number of tasks to resolve our dependency in a central location:

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

As shown in Figure 6 we register using Fetcher.self as a service Protocol (abstract type as compared to the previous example) and resolve it using the container. This is a simple example:

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

As shown in Figure 7 we have two classes that implement the same Fetcher protocol and the question is how to register the same Service Type Protocol and this is where Named Registration in a DI Container comes to play. As you can see we register two Fetcher with name parameter in which one return FetchFromDatabase instance and other FetchFromServer instance. When resolving we do the same thing:

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

As shown in Figure 8 Now Our Object that needs to be resolved by DI container needs to have some dynamic configuration This is where Registration with Arguments in a DI Container comes into play. The factory closure passed to the register method can take arguments that are passed when the service is resolved. When you register the service, the arguments can be specified after the Resolver parameter. Note: you can pass up to 9 arguments in Swinject.

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

Now we understand the basics and syntax of Swinject. This is how Figure 4 would look like when using Swinject

As shown in Figure 9 we created sharedContainer a property in Container class using extension which will return container singleton object until all registration have been done. We told the container how to create an instance of DataFetcher in its factory closure of register method.

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

As shown in Figure 10 we call the resolve method on client class and telling it to create an instance of DataFetcher now container will execute its factory method and after it resolved its dependency it will return DataFetcher instance. As said earlier benefits of DI container is to “resolve Complex dependency” and also we achieved a central location whose responsibility is to create an instance of an object after resolving all its dependencies.

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

Now every time you call resolve on DataFetcher.self it will always return a new instance of DataFetcher object as shown in Figure 11. What if we want the same instance this is where Object Scopes comes into play:

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

Object Scopes

Graph (the default scope) → It always creates a new instance when you call resolve but while resolving graph it shared the instances.

When client call resolve on A these are the action performed by the resolver:

As shown in Figure 12 B address is the same on A and C classes.

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

Transient → which always returns a new object. (an instance provided by a container is not shared).

As shown in Figure 13 by making B scope transient we are saying that doesn’t share it’s instance as shown in Console B address is different on both A and C instances:

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

Container → This scope is also known as Singleton. which means once the object is resolved it will share that instance for every resolve method until the application terminates as shown in Figure 14:

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

As shown in Figure 15 we performed a number of tasks to implement Custom Scopes:

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

For more information about scopes see this link. We didn’t cover the weak scope type.

Some Points

Registration Keys → A registration of the component for a given service is stored in a container with an internally created key. The container uses the key when trying to resolve a service dependency.

Value Types injection → All previous examples we saw uses classes but can resolve Struct as well Note: weak scope will not be available on Value types.

Modularizing Service Registration

Sometime in future, you can see this in your code if you don’t properly modularize your service registration as shown in Figure 16. This is where Assembler comes into play:

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

Assembly

The Assembly is a protocol that is provided a shared Container where service definitions can be registered. Let’s create Assembly to modularize and make readable service registration process as shown in Figure 17 and 18. Note: Any assembly can resolve any other assemble object if they have the same Assembler:

Источник

Пишем фотоприложение для iOS с нуля: большой туториал

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что это

Для работы нам понадобится среда разработки XCode. Код проекта лежит в репозитории на Github.

Обзор используемых библиотек

Swinject

Swinject является одним из самых популярных фреймворков для управления зависимостями на iOS.

Наиболее распространено использование вместе со сторибордами и расширением SwinjectStoryboard, но мы работаем с View – слоем исключительно в коде, поэтому не будем использовать дополнительные расширения к этому фреймворку.

PluggableAppDelegate

Работая над разными проектами, можно часто наблюдать перегруженный AppDelegate.swift. Он грязный, с беспорядочными инициализациями, настройками разных библиотек, используемых в проекте. Когда открываешь такой AppDelegate в первый раз сразу хочется его закрыть и больше не видеть.

При использовании PluggableAppDelegate вы разделяете AppDelegate на небольшие части, и в каждой такой части вы описываете небольшую функциональность.

Затем в AppDelegate вы просто указываете, какие ApplicationService подключать.

RxSwift — FRP-фреймворк. Добавляет возможность «реактивного стиля» программирования. Реактивное программирование — парадигма программирования, ориентированная на потоки данных и распространение изменений. Будем использовать Rx для биндинга UI элементов, таких как коллекции и текстовые поля.

В этом нам поможет расширение RxCocoa, предназначенное для работы с UI в реактивном стиле.

Firebase

Firebase — это набор облачных сервисов от Google, такой BaaS.

На самом деле Firebase предоставляет много возможностей, таких как:

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

Отдельно хочется выделить еще одну библиотеку, подключаемую в этом блоке, это FirebaseFirestoreSwift.

Это библиотека от стороннего разработчика, она позволяет нам производить преобразования в Firebase из Response в Codable и обратно.

EasyPeasy

Верстая UI в коде и используя Autolayout мы должны описывать много правил позиционирования элементов. Так вот EasyPeasy позволяет делать это кратко и элегантно.

Toast-Swift

Небольшая библиотека для отображения сообщений пользователю в UI.

SVProgressHUD

Красивая замена UIActivityIndicatorView с обильными возможностями для конфигурации внешнего вида.

Paparazzo

Удобнейший пикер изображений. Это такая мощная замена не всегда подходящему UIImagePickerController. К тому же он локализируется и настраивается с помощью встроенных тем. А еще на выходе Paparazzo отдает объект-обертку над ImageSource вместо UIImage, что позволяет нам иметь больше возможностей, например, при отображении превью выбранного изображения (установка размера изображения, заглушке). ImageSource — это такая абстракция над UIImage.

Кроме прочего, Paparazzo позволяет поворачивать, обрезать изображение после выбора из галереи или снимка с камеры, а также применять фильтры на изображения. Фильтрами мы пользоваться в рамках этого приложения не будем, но все равно приятно, что есть запас такой функциональности.

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

Начинаем!

Создаем пустой проект, выбираем Single View App. Для установки всех необходимых библиотек интегрируем CocoaPods:

Дальше переходим в папку проекта. Инициализируем рабочее пространство для нашего приложения:

Подключаем нужные библиотеки

Теперь в директории проекта появился файл с ProjectName.xcworkspace, открываем его. Находим в структуре проекта файл Podfile и начинаем заполнять нужными нам библиотеками:

Первая часть — core — наработанный набор библиотек, ускоряющих и упрощающих разработку. Здесь подключаются библиотеки, несущие основную функциональность приложения.

Вторая часть — firebase services. В качестве сервиса для аналитики был выбран Firebase, позволяющий отслеживать активность пользователей и собирать по ним аналитику. Здесь же подгружаются все необходимые библиотеки от Google.

Третья часть — ui — библиотека для верстки и работы с медиафайлами, в которой отображаются уведомления. Если ищете альтернативы UIActivityIndicatorView, то вот она.

Заполним Podfile и снова идем обратно в терминал: команда pod install добавляет все указанные нами библиотеки.

Базовая структура проекта

Генерация модулей

Поскольку в качестве архитектурного паттерна выбрано некое подобие MVP, используем кодогенерацию модулей с помощью библиотеки Generamba. Generamba позволяет генерировать файлы классов и автоматически класть в определенные папки и группы проекта. Устанавливаем библиотеку и запускаем её:

Отвечаем на все вопросы конфигуратора:

По окончании установки генерируется Rambafile, в который пропишем все конфигурации для генерации кода. Файл лежит в корневой директории проекта. Должен получиться файл следующего содержимого:

Нас интересуют только последние строки, где мы указываем название нашего MVP шаблона модуля и откуда его качать. В качестве шаблона используем нашу наработку:

Сохраняем файл и устанавливаем шаблоны:

Generamba подтянула шаблон и готова генерировать заготовки кода под модули. Сделаем первый. Пусть это будет Guest модуль — будем показывать его неавторизованным пользователям.

Снова идем в терминал:

Документацию по генерамбе можно найти в ее репозитории на гитхабе.

Там же есть описание, как подготавливать свои темплейты для модулей. А у нас готов гостевой модуль, его мы показываем всем пользователям при первом запуске.

Дробим AppDelegate

Первый модуль есть, но это пока только заготовка.

Более того, этот generamba-шаблон модуля подразумевает наличие некоторых протоколов в проекте, например, протокола Navigator, но об этом дальше.

Сейчас займёмся дроблением AppDelegate на отдельные сервисы.

Зачем это нужно? Это наработанная со временем техника разделения конфигурирования разных библиотек при запуске приложения. Зачем мешать Firebase, работу с пуш-уведомлением, конфигурирование логгеров и много чего еще. в одном методе AppDelegate? В конце концов, можно же использовать принцип разделения интерфейса.

В этом нам поможет библиотека PluggableAppDelegate.

В нашем случае мы создадим RootApplicationService для настройки рут-контроллера для window.

Также создадим GoogleApplicationService, в котором произведём инициализацию библиотек Firebase.

Код самого AppDelegate.swift будет теперь выглядеть так:

Зарегистрировали сервисы и разгрузили сам AppDelegate от кучи кода, смешанного в одном методе.

DI-контейнер

Для управления зависимостями будем использовать Swinject.

Настало время написать код в только что созданном RootApplicationService.

Добавляем приватные константы assembler и assemblies.

Позже в assemblies мы будем добавлять assembly каждого нового модуля.

Пока добавим туда лишь assembly нашего единственного Guest модуля.

Отлично! Дальше в didFinishLaunchingWithOptions укажем ассемблеру, что нужно применить все assemblies: это зарегистрирует наши зависимости для каждого модуля. Правда, мы еще ни одной не написали, но скоро это исправим.

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

Пока оставим здесь только загрузку гостевого модуля, позднее вернемся сюда и напишем логику определения, какой модуль грузить.

Сервисы

Поскольку у нас проект на MVP, бизнес-логику будем держать в сервисах.

Напишем первый протокол сервиса пользователя.

Добавляем группу /Source/Services и в нее файл UserService.swift

Сначала опишем поведение в протоколе:

Подключим RxSwift и Firebase

Свойство trigger нужно нам для отправки событий о статусе авторизации пользователя, это PublishSubject из RxSwift. Добавим немного подхода «Наблюдатель» в приложение, это позволит писать меньше кода. Он реализует механизм, который позволяет объекту этого класса получать оповещения об изменении состояния других объектов и тем самым наблюдать за ними.

Добавим также Enum AuthStatus для передачи самого статуса в триггер.

Дальше пишем реализацию методов, описанных в протоколе.

Это будет, наверное, самый маленький сервис в нашем приложении.

Теперь нам нужно зарегистрировать наш сервис в DI-контейнере.

Создаем ServicesAssembly.swift в группе Services и регистрируем наш только что написанный сервис.

Теперь идем в RootApplicationService и добавляем в assemblies нашу ServicesAssembly перед guestAssembly.

Firebase

Для идентификации пользователей мы используем Firebase Auth.

Давайте напишем реализацию методов регистрации и авторизации пользователя с помощью Firebase Auth.

Идем в наш UserService.swift и пишем реализацию протокола сервиса.

Свойство currentUser вычисляемое и отдает текущего пользователя, заполняя поля из объекта пользователя Firebase, если он авторизован.

Структура пользователя выглядит так:

Также мы добавили свойство isFirstLaunch, говорящее нам, в первый раз запускается приложение или нет.

Его значение мы храним просто в UserDefaults. UserDefaults – это интерфейс, работающий с настройками пользователя по умолчанию. Подробнее можно прочитать по теме тут.

Теперь наш файл сервиса UserService полностью выглядит так:

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что этоТак будет выглядеть экран авторизации.

Главный экран

Пришло время добавить еще один модуль, модуль главного экрана.

Набираем в терминале уже знакомую нам команду генерации заготовки под модуль:

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

Создадим структуру Post следующего вида:

Добавим расширениям соответствие протоколу Codable и опишем соответствие свойств объекта Post ключам в json, который будем получать с бэкенда.

Возвращаемся в наш HomeView, добавляем ему свойство для обновления данных с помощью RxSwift:

Теперь пропишем биндинг таблицы и нашего поля data:

Здесь мы указываем, какой класс ячейки использовать. В нашем случае это PostCell. Давайте посмотрим на его код:

Класс ячейки PostCell содержит, помимо прочего, свойство value, при изменении значения которого и происходит заполнение ячейки данными с помощью метода fillUI(post: Post).

Таким образом, как только мы присваиваем свойству value ячейки, она заполняется, и наша таблица отрисовывает данные.

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что этоВ реальной жизни интерфейс главной страницы будет выглядеть так.

Добавление публикации

Теперь давайте создадим еще один модуль — экран добавления публикации. Идем в терминал и набираем:

На этом экране добавляем UITextField для ввода названия публикации и UIImageView для отображения превью выбранного изображения.

Давайте пока отложим работу в модуле CreatePost и вернемся в модуль главного экрана.

Добавим главному экрану кнопку создания публикации.

Идем в HomeViewController и добавляем метод setupNavigationBar:

Этот метод мы будем вызывать во viewDidLoad, чтобы «настроить» наш UINavigationBar сразу после того, как вью загрузилась.

В качестве селектора указан метод addAction. Этот метод просто «говорит» презентеру, что нужно показать медиапикер.

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

Посмотрим на код навигатора:

Чтобы следовать протоколу Navigator, реализуем методы makeViewController(for:) и navigate(to:)

В createViewController(for:) проверяем destination и в случае, когда нам нужно показать пикер, инициализируем PaparazzoViewController с настройками:

Еще у нас есть completion замыкание, чтобы показать пикер. Указываем выполнение этого замыкания в блоке onFinish при инициализации PaparazzoViewController.

Дальше просто показываем в модальном режиме наш сконфигурированный PaparazzoViewController, получаем медиапикер с возможностью выбрать изображение из фотогалереи или сделать снимок.

swinject swift что это. Смотреть фото swinject swift что это. Смотреть картинку swinject swift что это. Картинка про swinject swift что это. Фото swinject swift что этоЭкран пикера будет выглядеть так

Теперь возвращаемся в презентер главного модуля — в то место, где мы получаем сигнал от вью, что нам нужно показать пикер, и напишем следующий код:

Здесь мы указываем, что сразу после того, как отработал медиапикер, и у нас в completion блоке есть items, мы выбираем первый айтем. А как мы помним, у нас медиапикер сконфигурирован на выбор только одного изображения. Далее переходим с этим айтемом в экран createPost.

Там подставляем выбранное изображение в форму добавления публикации.

Нам остается только ввести название публикации и нажать «Сохранить».

Провайдер

Мы создали структуру Post и заполнили её данными: название и изображение.

Теперь нам нужно сохранить нашу публикацию на бэкенде, в нашем случае в Firebase.

Для этого опишем провайдера для работы с публикациями:

Здесь мы описали провайдер протоколом и реализовали методы протокола в классе имплементации.

У нас получилось только два метода: один для получения списка публикаций, второй для сохранения публикации.

Теперь нужно зарегистрировать наш провайдер в DI контейнере.

Затем добавляем ProvidersAssembly в список assemblies в RootApplicationService:

PostService

Теперь нам нужно соединить только что созданный и заполненный объект публикации и провайдера для сохранения публикации на бекенде.

Для этого нам нужен PostService.

Как раз здесь мы и обращаемся к провайдеру, когда нам нужно получить публикации, чтобы отобразить их на главном экране, или когда нам нужно сохранить публикацию.

Не забываем зарегистрировать PostService в ServicesAssembly:

Здесь у нас ещё есть StorageService — это сервис для работы с файлами. Его код можно посмотреть в проекте.

Итого

Мы сделали приложение, в котором можно авторизоваться, увидеть ленту пользователя, снять фото или загрузить из библиотеки. Его можно развивать как угодно: от допиливания функциональности соцсетей до ухода в группировку фотографий для определенных категорий пользователей, создания фотоальбомов, страниц пользователей и так далее.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *