viper - чистая архитектура ios-приложения (И. Чирков)
TRANSCRIPT
VIPER - чистая архитектура iOS приложения
Проблемы, возникающие при использовании MVC, и их решение при помощи VIPER.
ПланПроблемы MVC
Структура VIPER модуля
Сервисы
Data flow в модуле
Навигация
Data flow между модулями
Вложенные модули
Код
Что такое MVC?
Что такое MVC?
MVC - Massive View Controller!
MVC
Чем должен занимается Controller?
Обновлять данные на View
Ловить события, генерируемые пользователем
MVC в реальности
Чем приходится занимается Controller-у
Обновлять данные на View
Ловить события, генерируемые пользователем
Являться делегатом разнообразных сервисов
Обрабатывать полученные данные
Отвечать за навигацию между экранами
Отвечать за поток данных между экранами
...
Чем приходится занимается Controller-у
Обновлять данные на View
Ловить события, генерируемые пользователем
Являться делегатом разнообразных сервисов
Обрабатывать полученные данные
Отвечать за навигацию между экранами
Отвечать за поток данных между экранами
...
Massive View Controller
Последствия Massive View Controller
Огромные классы
Нарушение принципов SOLID (куча ответственностей)
Сложно дебажить
Сложно вносить изменения
Сильная связность компонентов
Сложно/невозможно тестировать
...
VIPERClean Architecture iOS приложения
VIPER
View
Interactor
Presenter
Entity
Router
Структура VIPER модуля
Структура VIPER модуля
View Presenter Interactor Router
override func viewDidLoad() { super.viewDidLoad() self.title = "Title"}
func setupTitle(title: String) { self.title = title}
override func viewDidLoad() { super.viewDidLoad() presenter.setup()} func setup() {
view.setupTitle("Title")}
View Presenter Interactor Router
@IBAction func validateTouchUpInside() { let email = self.emailField.text if self.validateEmail(email) { self.presentSuccessScreen() } }@IBAction func validateTouchUpInside()
func validateButtonPressed(email: String)
func validateEmail(email: String) -> Bool
func presentSuccessScreen()
if () {}
Сервисы
Разбиваем интерактор на сервисы
Сервисы
Data flow
Data flow
Навигация
SettingsModule GeneralSettingsModule
SettingsModule GeneralSettingsModule
protocol SettingsDisplayManagerDelegate: class { // DISPLAY MANAGER DELEGATE func didSelectCell()}
class SettingsDisplayManager: NSObject, UITableViewDelegate { // DISPLAY MANAGER weak var delegate: SettingsDisplayManagerDelegate? // VIEW func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { delegate?.didSelectCell() }}
class SettingsView : UIViewController, SettingsDisplayManagerDelegate { // VIEW var output: SettingsPresenter! var displayManager: SettingsDisplayManager! func didSelectCell() { output.didSelectCell() }}
class SettingsPresenter { // PRESENTER var view: SettingsView! var router: SettingsRouter! func didSelectCell() { router.presentGeneralSetting() } }
class SettingsRouter { // ROUTER var view: SettingsView! func presentGeneralSetting() { let generalSettingsModule = GeneralSettingsModule() //Инициализируем модуль GeneralSettingModule let generalSettingsView = generalSettingsModule.view // Забираем у него View view.navigationController?.pushViewController(generalSettingsView, animated: true) }}
DataFlow между модулями
NewsModule DetailModule
NewsID
NewsModule DetailModule
[NewsItem]
[NewsItem]
[NewsItem] [NewsItem]NewsID
NewsID
NewsID
NewsID
NewsIDNewsItem
NewsItem
Done
NewsModule DetailModule
NewsID
NewsID
NewsModule DetailModule
NewsID NewsID
outputHandler outputHandler
class NewsPresenter: DetailModuleOutput { // PRESENTER func didSelectNews(newsId: Int) { router.presentDetails(self) } func newsAddedToFavorites(newsId: Int) { }}
class NewsRouter { // ROUTER var view: NewsView! func presentDetails(outputHandler: DetailModuleOutput) { let detailModule = DetailModule(outputHandler: DetailModuleOutput)//Инициализируем модуль let detailModuleView = detailModule.view // Забираем у него View view.navigationController?.pushViewController(detailModuleView, animated: true) }}
protocol DetailModuleOutput: class { // OutputHandler Protocol func newsAddedToFavorites(newsId: Int)}
class DetailPresenter { // PRESENTER weak var outputHandler: DetailModuleOutput? func newsAddedToFavorite(newsId: Int) { // Метод вызван интерактором (DetailInteractor) outputHandler?.newsAddedToFavorites(newsId) }}
class NewsPresenter: DetailModuleOutput { // PRESENTER func didSelectNews(newsId: Int) { router.presentDetails(self) }
func newsAddedToFavorites(newsId: Int) { // DONE! }}
Вложенные модули
Данные мастера
Портфолио
Услуги
Расписание
Отзывы
Немного кода
Инициализация модуля
let newsModule = NewsModule() //Инициализируем модуль NewsModule let newsView = newsModule.view // Забираем у него View view.navigationController?.pushViewController(newsView, animated: true)
class NewsModule: NSObject { private var viewController: NewsViewController? var view: UIViewController { guard let view = viewController else { viewController = NewsViewController(nibName: "NewsViewController", bundle: nil) configureModule(viewController!) return viewController! } return view }
private func configureModule(view: NewsViewController) { // Устанавливает зависимости модуля. let presenter = NewsPresenter() let router = NewsRouter() let interactor = NewsInteractor() router.view = view view.output = presenter view.router = router presenter.view = view presenter.router = router presenter.interactor = interactor interactor.output = presenter }}
Viewprotocol NewsViewInput: class { func updateView(news: [NewsItem])}
protocol NewsViewOutput: class { func setupView()}
class NewsViewController: UIViewController, NewsViewInput, NewsDisplayManagerDelegate { var output: NewsViewOutput! // Ссылка на Presenter let displayManager = NewsDisplayManager()
override func viewDidLoad() { super.viewDidLoad() output.setupView() // View сообщает Presenter-у о готовности }
func updateView(news: [NewsItem]) { // View получила от Presenter-а массив моделей новостей displayManager.updateTable(news) }}
Presenterclass NewsPresenter: NewsViewOutput, NewsInteractorOutput { weak var view: NewsViewInput! var router: NewsRouter! var interactor: NewsInteractorInput!
func setupView() { interactor.obtainNews() // Presenter запрашивает список новостей у Interactor-a }
func newsObtained(cards: [CardItem]) { if news.count == 0 { view.showPlaceholder() } else { view.hidePlaceholder() view.updateView(news) // Presenter отправляет список полученных новостей View-шке } }
}
Interactorprotocol PaymentInteractorInput: class { func obtainNews()}
protocol PaymentInteractorOutput: class { func newsObtained(news: [NewsItem]) func errorReceived(message: String)}
class NewsInteractor: NewsInteractorInput { weak var output: NewsInteractorOutput! let newsService = NewsService()
func obtainNews() { newsService.obtainNews( success: { news in self.output.newsObtained(news) }, failure: { error in self.output.errorReceived(error.localizedDescription) } ) }}
Routerclass NewsRouter { weak var view: UIViewController!
func presentDetails(newsId: Int) { let detailModule = DetailModule() //Инициализируем модуль NewsModule let detailView = detailModule.view // Забираем у него View view.navigationController?.pushViewController(detailView, animated: true) }
func presentError(message: String) { let alert = UIAlertController(title: "ERROR_TITLE".localized, message: message, preferredStyle: .Alert) alert.addAction(UIAlertAction(title: "OK".localized, style: .Default, handler: nil)) view.presentViewController(alert, animated: true, completion: nil) }
}
Файлы VIPER модуляViewInputProtocol
ViewOutputProtocol
View
Presenter
Router
InteractorInputProtocol
InteractorOutputProtocol
InteractorИтого: 8 файлов на один модуль! о_О
Кодогенерация. Vipergen
Vipergenhttps://github.com/nsleader/vipergen
VIPERClean Architecture iOS приложения