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
Огромные классы
Нарушение принципов SOLID (куча ответственностей)
Сложно дебажить
Сложно вносить изменения
Сильная связность компонентов
Сложно/невозможно тестировать
VIPER
Clean 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-шке
}
}
}
Interactor
protocol 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
Vipergen
https://github.com/nsleader/vipergen
VIPER
Clean Architecture iOS приложения

Viper - чистая архитектура iOS-приложения (И. Чирков)

  • 1.
    VIPER - чистаяархитектура iOS приложения Проблемы, возникающие при использовании MVC, и их решение при помощи VIPER.
  • 2.
    План Проблемы MVC Структура VIPERмодуля Сервисы Data flow в модуле Навигация Data flow между модулями Вложенные модули
  • 3.
  • 4.
  • 5.
    MVC - MassiveView Controller!
  • 6.
  • 7.
    Чем должен занимаетсяController? Обновлять данные на View Ловить события, генерируемые пользователем
  • 8.
  • 9.
    Чем приходится занимаетсяController-у Обновлять данные на View Ловить события, генерируемые пользователем Являться делегатом разнообразных сервисов Обрабатывать полученные данные Отвечать за навигацию между экранами Отвечать за поток данных между экранами
  • 10.
    Чем приходится занимаетсяController-у Обновлять данные на View Ловить события, генерируемые пользователем Являться делегатом разнообразных сервисов Обрабатывать полученные данные Отвечать за навигацию между экранами Отвечать за поток данных между экранами
  • 11.
    Последствия Massive ViewController Огромные классы Нарушение принципов SOLID (куча ответственностей) Сложно дебажить Сложно вносить изменения Сильная связность компонентов Сложно/невозможно тестировать
  • 12.
    VIPER Clean Architecture iOSприложения
  • 13.
  • 14.
  • 15.
  • 16.
    View Presenter InteractorRouter 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") }
  • 17.
    View Presenter InteractorRouter @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 () {}
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
    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() } }
  • 25.
    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) } }
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
    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) }
  • 32.
    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! } }
  • 33.
  • 35.
  • 36.
  • 37.
    Инициализация модуля let newsModule= NewsModule() //Инициализируем модуль NewsModule let newsView = newsModule.view // Забираем у него View view.navigationController?.pushViewController(newsView, animated: true)
  • 38.
    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 } }
  • 39.
    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) } }
  • 40.
    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-шке } } }
  • 41.
    Interactor protocol 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) } ) } }
  • 42.
    Routerclass NewsRouter { weakvar 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) } }
  • 43.
  • 44.
  • 47.
  • 48.
    VIPER Clean Architecture iOSприложения

Editor's Notes

  • #7 Как видят MVC в Apple Бизнес логика в модели (активная модель) За отображение отвечает данных отвечает View
  • #8 Контроллер ловит и обрабатывает пользовательские действия (нажатия, жесты и тд), а также является посредником между бизнес логикой и представлением
  • #9 В реальности же мы видим что-то подобное
  • #11 Все это приводит к массивному View Controller-у
  • #12 Как со всем этим бороться?
  • #13 VIPER - подход к архитектуре iOS приложения, где логическая структура делится на различные уровни обязанностей.
  • #14 Слово VIPER является акронимом
  • #15 В чем суть? Приложение делится на логические единицы - VIPER модули. Модуль - законченная логическая единица (юзер стори). Например, модулем может являться экран приложения (экран авторизации, экран профиля пользователя, ...) Модуль делится на 5 компонентов.
  • #16 View - отвечает за отображение данных на экране и перехват пользовательский действий. Не занимается логикой. Пересылает события другим компонентам. Interactor. В интеракторе содержится вся бизнес логика модуля. Работа с базой данных, сетью, валидацией, вычисления. Presenter. Связующее звено между компонентами модуля. Получает информацию о действиях пользователя и направляет их в интерактор за логикой или в роутер. Ловит уведомления от интерактора, преобразовывает их в структуру удобную для отображения во вью Router. Отвечает за роутинг - навигацию внутри приложения. Entity - Модельные объекты. Данные с которыми мы работаем в приложении.
  • #17 Пример установка заголовка (тайтла)
  • #18 Пример валидация емейла и отображение экрана с успехом
  • #19 Интерактор выполняет слишком много работы. Сложно переиспользовать.
  • #20 Сервис - компонент, выполняющий какую-то одну задачу. Например, сервис авторизации, сервис профиля, новостей, … Сервисы ничего не знаю об интеракторе. Интерактор всего лишь делегирует задачи сервисам и ждет результат. Сервисы легко тестировать.
  • #21 На примере авторизации
  • #22 Таблица DisplayManager View получает данные, обновляет DisplayManager, DisplayManager сообщаят View о том что она обновлена. View релодит таблицу Нажатие на ячейку
  • #23 Простейший пример - нужно запушить новый экран. Роутер SettingsModule презентит модуль GeneralSettingModule
  • #24 За навигацию отвечает Router
  • #25 За навигацию отвечает Router
  • #26 За навигацию отвечает Router
  • #31 Презентер модуля DetailModule должен иметь ссылку на NewsModule. Это осуществляется посредствам outputHendler
  • #36 Экран можно разбить на несколько модулей (5 штук) Родительский модуль Сабмодули Сабмодули являются чайлд вью котроллерами. Чайлд вью контроллер добавляется через роутер родительского модуля
  • #49 Подход к архитектуре iOS приложения, где логическая структура делится на различные уровни обязанностей.