- PVSM.RU - https://www.pvsm.ru -
В последние годы заметно набрала обороты тема альтернативных архитектур для создания приложений под платформу iOS. На доске особого почета уже закрепились некоторые силачи, известные как MVP, MVVM, VIPER. А кроме них есть еще множество других, не столь распространенных.
Среди силачей, на мой взгляд, ни одна не является универсальной таблеткой для всех случаев:
Есть вариант использовать несколько архитектур, ибо многие позволяют в той или иной степени сочетать себя с другими, но это тоже не слишком удобно как минимум по трем причинам:
И вот, столкнувшись за последние четыре года со множеством проектов (несколько проектов из банковской сферы, несколько разнородных заказных, а также несколько своих собственных — как приложений, так и игровых), я сформировал для себя архитектурный подход, который теперь по возможности стараюсь использовать в любом проекте, который начинаю.
Пока что он меня не подводил. При этом не думаю, что я первопроходец: наверняка, многие уже используют аналогичный подход. Но поскольку в проектах, с которыми сталкивался лично я, с архитектурой было довольно непросто, я захотел поделиться своими соображениями.
При моем формировании этого варианта архитектуры учитывались некоторые ключевые аспекты:
В итоге:
Основные части архитектуры:
Я условно называю ее SILVER: по первым буквам.
Соберем небольшое показательное приложение, которое будет вести список стран и городов, которые мы сами же и вспомним, надеясь на собственные познания в географии.
Для начала посмотрим публичное представление любого модуля. В данной фразе модулем представляется некий собирательный образ, которым можно управлять, и состояние которого можно отобразить на экране. Итак, в любом модуле есть две публичные части:
protocol IBaseRouter: class {
var viewController: UIViewController { get }
}
struct Module<RT> {
let router: RT
let viewController: UIViewController
}
Здесь может появиться вопрос, зачем я повторил ViewController в отдельное свойство структуры, если они итак связаны.
Причина кроется в том, что для обеспечения максимально простого управления памятью упор смещен на то, что ViewController владеет сильными связями с остальными частями модуля: когда делается возврат с текущего экрана назад, то происходит удаление ViewController из иерархии UIKit, а вместе с ним удобно умирает и весь модуль.
По этой же причине из родительского модуля cвязи с дочерними Router делаются слабыми, в случае если вообще понадобятся.
Так вот, чтобы не засорять память, ViewController в первый раз создается только в тот момент, когда к нему происходит обращение. И таким образом получается, что для того, чтобы появился жизнеспособный модуль, нужно обратиться к его ViewController. Однако, для возможности получения управления, общаться нужно с его Router.
Если из фабрики модуля получить Router, то мы не будем обладать сильной ссылкой на модуль, и он будет уничтожен уже на следующей строчке кода. А если из фабрики получить ViewController, то мы не будем обладать возможностью управления и настройки модуля.
Эту проблему и решает структура Module, которая заполняется в момент создания модуля, и позволяет временно держать сразу обе сильные ссылки — на Router и на ViewController. В результате, пока структура жива в локальной области видимости, Router можно сохранить в слабую ссылку, а ViewController отобразить на экране, где UIKit придержит на него ссылку сильную.
func InputModuleAssembly(title: String, placeholder: String, doneButton: String) -> Module<IInputRouter> {
let router = InputRouter(title: title, placeholder: placeholder, doneButton: doneButton)
return Module<IInputRouter>(router: router, viewController: router.viewController)
}
private func presentCountryInput() {
let module = InputModuleAssembly(title: "Add city", placeholder: "Country", doneButton: "Next")
self.countryInputRouter = module.router
module.router.configure(
doneHandler: { [unowned self] country in
self.interactor.setCountry(country)
self.presentNameInput()
}
)
internalViewController?.viewControllers = [module.viewController]
}
В целом, Router нужен для того, чтобы:
protocol IInputRouter: IBaseRouter {
func configure(doneHandler: @escaping (String) -> ())
}
final class InputRouter: IInputRouter {
private let title: String
private let placeholder: String
private let doneButton: String
let interactor: IInputInteractor
private weak var internalViewController: IInputViewController?
init(title: String, placeholder: String, doneButton: String) {
self.title = title
self.placeholder = placeholder
self.doneButton = doneButton
interactor = InputInteractor()
}
var viewController: UIViewController {
if let _ = internalViewController {
return internalViewController as! UIViewController
}
else {
let vc = InputViewController(title: title, placeholder: placeholder, doneButton: doneButton)
vc.router = self
vc.interactor = interactor
internalViewController = vc
interactor.view = vc
return vc
}
}
func configure(doneHandler: @escaping (String) -> ()) {
internalViewController?.doneHandler = doneHandler
}
}
На случай, если в модуле может быть произведено несколько действий, метод настройки может содержать все возможные callback. Это позволит в случае добавления новых callback в процессе разработки не забывать прописать их вызов тоже.
// Так сложно забыть прописать дополнительный callback,
// поскольку компилятор не соберет приложение,
// если будет вызван метод со старым набором параметров.
func configure(cancelHandler: @escaping () -> (),
doneHandler: @escaping (String) -> ())
// А так можно забыть дописать второй callback рядом с теми местами,
// где в коде уже используется первый.
func configure(cancelHandler: @escaping () -> ())
func configure(doneHandler: @escaping (String) -> ())
Точно таким же образом, в виде хранимого модуля, может быть представлен и сам старт приложения, который получается таким образом довольно лаконичным:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private weak var rootRouter: IRootRouter!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
let module = RootModuleAssembly(window: window)
rootRouter = module.router
window.rootViewController = module.viewController
window.makeKeyAndVisible()
return true
}
}
Зависимости идут от ServiceLocator, который настраивается в RootRouter (хотя, для чистоты логики, возможно стоит перенести его в RootInteractor), и с ним связано два главных нюанса:
В рамках SILVER предполагается, что модуль Root есть всегда, поскольку в рамках его ответственности как минимум:
struct ServiceLocator {
let geoStorage: IGeoStorageService
func prepareInjections() {
prepareInjection(geoStorage)
}
}
func inject<T>() -> T! {
let key = String(describing: T.self)
return injections[key] as? T
}
fileprivate func prepareInjection<T: Any>(_ injection: T) {
let key = String(describing: T.self)
injections[key] = injection
}
final class RootRouter: IRootRouter {
// ...
init(window: UIWindow) {
let serviceLocator = ServiceLocator(
geoStorage: GeoStorageService()
)
serviceLocator.prepareInjections()
}
// ...
}
final class ListInteractor: IListInteractor {
// ...
private lazy var geoStorageService: IGeoStorageService = inject() // pretty easy!
// ...
}
Автор: Станислав Потемкин
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/266903
Ссылки в тексте:
[1] Посмотреть демо-проект на GitHub: https://github.com/bronenos/silver-arch
[2] Источник: https://habrahabr.ru/post/341178/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.