Model Layer
- Managable: можно сохранять в БД
- Parameters: параметры запросов
- Responses: ответы на запросы по API
- UniversalModel: любые параметры/ответ любого запроса
📄 Вся архитектура завязана на протоколах, что обеспечивает подмену каких-либо модулей без нарушения работы приложения.
Для VC
создается протокол ХRenderingLogic
, реализующий логику отображения данных.
Для Provider
создается протокол ХProvisionLogic
, реализующий логику получения данных из слоя бизнес-логики.
Для Presenter
создается протокол ХPresentationLogic
, ссылка на который передается в Provider для последующего вызова.
Для Router
создается протокол ХRoutingLogic
, организующий логику переходов от модуля Х
в другие модули
-
class XAssebly
Основная задача - собрать модуль. Тут регистрируются View, Provider, Presenter, Router -
struct XModule
- class Convertor - конвертирует storyboard, возвращая название ViewController'a.
- class Builder
- может быть разным в зависимости от иерархии навигации модуля
- создать/вытащить из сториборда ViewController, чтобы мы могли передать туда InitialState
-
Есть 2 состояния:
- Первоначальное - что передаем в модуль
- Финальное (есть не всегда) - то, что выдает модуль после обработки данных
Передаем данные в модуль и ожидаем обработки, получаем на выходе какие-то данные.
Отвечает только за конфигурацию и взаимодействие с Renderer. В контроллере не должно находиться никакой бизнес логики, взаимодействия с сетью, вычислений и так далее. Его задача обрабатывать события с View, отображать или отправлять данные (без обработки и проверок) в Provider.
Не содержит тяжелой логики, но управляет ей. Все события в основном приходят в него.
protocol ProvisionLogic - методы получения данных от Provider.
typealias UserInteraction - сбор всех протоколов interaction'ов, которые должны быть объявлены в Renderer
let moduleReference - ссылка на Assembly.
За жизненный цикл отвечает VC - moduleLifeCycleOwner
var lifeCycle - массив рендеров, которые хотят получить событие об изменении жизненного цикла VC.
Когда создаем VC, константа moduleReference будет инициализирована, срабатывает assembly и мы передаем туда тип модуля (Module.Type), который у нас соответственно и собирается (регистрируется при помощи register view, provider, router, presenter)
Объект части ViewController.
- Отвечает за отображение части экрана (например, cell или tabBar)
- Может обращаться к View и что-то получать
- Повторяет жизненный цикл VC (чтобы View не держала все Renderer и не говорила, что надо делать при изменении определенного события)
- Содержит в себе аутлеты, экшены
У каждого Renderer создается слабая ссылка на VC - delegate (он находится внутри Renderer). VC держит ссылку на renderer, чтобы показать интерфейс, а renderer держит ссылку на VC, чтобы сообщить об изменениям на интерфейсе.
Каждый Renderer понимает, что у него есть определенная интерактивность пользователя и он для этого создает интерфейс. View является делегатом этих событий.
set(content: ...) - контент определяется при помощи typealias ModelType
Состоит из 2-ух частей:
Начального состояния (initialState) и финального (finalState) - держит на них ссылки, они оба являются опциональными.
ModuleInitialState - набор данных с помощью которых модуль будет инициализирована
ModuleFinalState - набор данных, которые необходимо передать родительскому модулю, например, передать изображение после обработки
В state сохраняем то, что показываем на рендере, но копию
У Presenter есть ссылка на state в качестве readOnly
Обращаемся к сервису, получаем данные от бизнес-логики. Тут не должно быть логики!
Тут мы сообщаем какие-то проблемы, которые случились, что смогли получить, подписываемся на данные/изменения в БД и так далее.
Подготовка и преобразование данных и предоставление их View.
Есть ссылка на View, чтобы передать туда данные для показа.
Переходы между модулями.
📝 Сервис состоит из:
Отвечает за Dependency Injection сервисов, объявленных в слое Private через инициализатор init.
Сервисами можно считать, например:
- HTTP клиент
- Хранилище устройства.
Пример инъекции зависимостей:
func assemble(container: DIContainer) {
container.record(AccountService.self, inScope: .autoRelease) { resolver in
AccountServiceImplementation(resolver.unravel(RestIO.self),
resolver.unravel(Repository.self,
name: RepositoryAssembly.Names.encryption),
authKeychainNew)
}
}
Блок Public состоит из протокола xService
, описывающего функции, которые должны быть реализованны в блоке Private.
Протокол должен обязательно наследоваться от базового протокола BusinessLogicService
. Запрещено создание сервиса, без наследования от базового протокола.
Блок Private содержит в себе класс xServiceImplementation
, реализующий функции, описанные в протоколе xService
.
Так как протокол BusinessLogicService
(от которого наследован протокол xService
) содержит в себе инициализатор со всеми необходимыми зависимостями, то именно класс xServiceImplementation
их инициализирует через:
init(_ args: Any?...)
Если у сервера нет зависимости от других объектов, то передается пустой список аргументов.
В случае, если нужный объект зависимости не был найдет в списке аргументов, то процесс инициализации прерывается с фатальной ошибкой.
Также, помимо реализации функций класс xServiceImplementation
содержит в себе ссылки на HTTP клиент и/или Хранилище устройства, которые инициализируются в блоке Assembly.
Основная логика работы модуля начинается с View.
Допустим, нужно получить профиль и отобразить его.
Во View есть протокол ХProvisionLogic
. Добавляем туда метод obtainProfile(), который отвечает за получение профиля. Во viewDidLoad() пишем provider.obtainProfile()
и говорим: провайдер, получи профиль.
XProvider имплиментирует XProvisionLogic, который лежит во View.
В провайдер добавляются все сервисы, которые нужны. Это делается через DI, т.е пишется инициализатор, который может принимать сервисы в себя.
init(presenter: ProfilePresentationLogic,
accountService: AccountService,
unauthService: UnauthorizationService,
pushNotificationsService: PushNotificationService) {
self.presenter = presenter
self.accountService = accountService
self.unauthService = unauthService
self.pushNotificationsService = pushNotificationsService
}
В XAssembly нужно заинъектить их:
func registerProvider(in container: DIContainer) {
container.record(ProfileProvisionLogic.self) { (resolver, presenter: ProfilePresentationLogic) in
ProfileProvider(presenter: presenter,
accountService: resolver.unravel(AccountService.self)!,
unauthService: resolver.unravel(UnauthorizationService.self)!,
pushNotificationsService: resolver.unravel(PushNotificationService.self)!)
}
}
Все методы с сервиса возвращаются, как Promise<[...]>
func fetchProfile() -> Promise<Profile>
Для того, чтобы получить профиль, мы обращаемся к сервису, вызываем метод, а дальше можем использовать compactMap/done/catch и так далее, в зависимости от задачи.
func obtainProfile() {
accountService.fetchProfile().then { profile in
self.accountService.fetchNewsRole().map { ($0, profile) }
}.done { newsRoleResponse, profile in
self.presenter.didObtain(newsRole: newsRoleResponse.newsRole, profile)
}.catch { error in
self.presenter.encountered(error)
}
}
В файле провайдера есть XPresentationLogic
, где нам нужен didObtainProfile(), Profile - то, за чем мы ходили в провайдер и что было получено, например, с сервера или БД.
Делает View из бизнесовой модели.
func didObtain(_ countries: [Country]) {
let models = countries.map {
UICountry(name: $0.name,
holder: "#",
prifix: $0.phoneCode,
mask: $0.phoneMask,
flag: $0.flag)
}
view?.display(models)
}
Отображение профиля:
func display(_ profile: ProfileRenderer.ModelType, _ newsRole: NewsRoleResponse.Kind) {
renderer.set(content: profile)
stateRenderer.set(content: nil)
state.newsRole = newsRole
}