Programming
Development for iOS
Development of mobile applications
Swift
February 19

Сравнение архитектур Viper и MVVM: Как применить ту и другую



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


Данная статья является переводом статьи Rafael Sacchi «Comparing MVVM and Viper architectures: When to use one or the other». К сожалению, в какой-то момент создания статьи выставилась «публикация» вместо «перевода», поэтому приходится писать здесь.

Хорошо спроектированная архитектура очень важна для того, чтобы обеспечить длительную поддержку вашего проекта. В этой статье мы рассмотрим архитектуры MVVM и VIPER как альтернаитву традиционному MVC.

MVC является хорошо известной концепцией для всех тех, кто уже довольно давно занимается разработкой ПО. Данный паттерн делит проект на три части: Model, представляющий сущности; View, представляющий собой интерфейс для взаимодействием с пользователем; и Controller, ответственный за обеспечение взаимодействие между View и Model. Это архитектура, которую Apple предлагает нам использовать в наших приложениях.

Однако, вы вероятно знаете что проекты бывают с довольно большим и сложным функционалом: поддержка сетевых запросов, парсинг, доступ к моделям данных, преобразование данных для выдачи, реакции на интерфейсные события и т.д. В итоге вы получаете громадные контроллеры, которые решают вышеперечисленные задачи и кучу кода, который невозможно переиспользовать. Другими словами, MVC может стать кошмаром для разработчика при длительной поддержке проекта. Но как обеспечить высокую модульность и переиспользуемость в iOS-проектах?

Мы рассмотрим две очень известных альтернативы архитектуре MVC: MVVM и VIPER. Они обе довольно известны в iOS-сообществе и доказали что могут являться отличной альтернативой MVC. Мы поговорим об их структуре, напишем пример приложения и рассмотрим случаи когда лучше использовать ту или иную архитектуру.

Пример

Мы напишем приложение с таблицей контактов пользователей. Вы можете пользоваться кодом из данного репозитория. В папках Starter, содержится базовый скелет проекта, а в папках Final полностью готовое приложение.

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



Второй экран — это экран добавления нового контакта, с полями ввода имени и фамилии и кнопками Done и Cancel.



MVVM

Как это работает:

MVVM расшифровывается как Model-View-ViewModel. Этот подход отличается от MVC логикой распределения ответственности между модулями.

  • Model: Этот модуль не отличается от аналогичного в MVC. Он отвечает за создание моделей данных и может содержать в себе бизнес-логику. Вы также можете создать вспомогательные классы, например такие как manager-класс для управления объектами в Model и network manager для обработки сетевых запросов и парсинга.
  • View: И вот тут все начинает меняться. Модуль View в MVVM охватывает интерфейс(подклассы UIView, файлы .xib и .storyboard), логику отображения(анимация, отрисовка) и обработку пользовательских событий(нажатие кнопок и т.д.) В MVC за это отвечают View и Controller. Это означает что интерфейсы(views) у вас останутся неизменными, в то время как ViewController будет содержать малую часть того что было в нем в MVC и, соответственно, сильно уменьшится.
  • ViewModel: Это теперь то место, где будет располагаться большая часть кода, которая до этого была у вас в ViewController. Слой ViewModel запрашивает данные у Model(это может быть запрос к локальной базе данных или сетевой запрос) и передает их обратно во View, уже в том формате, в котором они будут там использоваться и отображаться. Но это двунаправленный механизм, действия или данные, вводимые пользователем проходят через ViewModel и обновляют Model. Поскольку ViewModel следит за всем что отображается, то полезно использовать механизм связывания между этими двумя слоями.


В сравнении с MVC, вы переходите от архитектуры, которая выглядит так:



К следующему варанту архитектуры:



В которой для реализации View используются классы и подклассы UIView и UIViewController.

Ну а теперь к делу. Давайте напишем пример нашего приложения, используя архитектуру MVVM.

MVVM Contacts App

MODEL

Следующий класс представляет собой модель контакта Contact:

import CoreData

open class Contact: NSManagedObject {
    
    @NSManaged var firstName: String?
    @NSManaged var lastName: String?
    
    var fullName: String {
        get {
            var name = ""
            if let firstName = firstName {
                name += firstName
            }
            if let lastName = lastName {
                name += " \(lastName)"
            }
            return name
        }
    }
}


У класса контакта есть поля firstName, lastName, а также вычисляемое свойство fullName.

VIEW

VIEW включает в себя: Main storyboard, с уже размещенными на нем вью; ContactsViewController, который отображает список контактов в виде таблицы; и AddContactViewController с парой лейблов и полей ввода, для добавления имени и фамилии нового контакта. Давайте начнем с ContactsViewController. Его код будет выглядеть следующим образом:

import UIKit

class ContactsViewController: UIViewController {

    @IBOutlet var tableView: UITableView!
    let contactViewModelController = ContactViewModelController()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.tableFooterView = UIView()
        contactViewModelController.retrieveContacts({ [unowned self] in
            self.tableView.reloadData()
        }, failure: nil)
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let addContactNavigationController = segue.destination as? UINavigationController
        let addContactVC = addContactNavigationController?.viewControllers[0] as? AddContactViewController

        addContactVC?.contactsViewModelController = contactViewModelController
        addContactVC?.didAddContact = { [unowned self] (contactViewModel, index) in
            let indexPath = IndexPath(row: index, section: 0)
            self.tableView.beginUpdates()
            self.tableView.insertRows(at: [indexPath], with: .left)
            self.tableView.endUpdates()
        }
    }

}

extension ContactsViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") as? ContactsTableViewCell
        guard let contactsCell = cell else {
            return UITableViewCell()
        }

        contactsCell.cellModel = contactViewModelController.viewModel(at: (indexPath as NSIndexPath).row)
        return contactsCell
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return contactViewModelController.contactsCount
    }

}


Даже при беглом взгляде видно, что данный клас реализует в большей части интерфейсные задачи. В нем также присутствует навигация в методе prepareForSegue(::) — и это как раз тот момент, который изменится в VIPER, при добавлении слоя Router.

Давайте теперь внимательнее посмотрим на расширение класса, которое реализует протокол UITableViewDataSource. Функции не работают напрямую с моделью контакта пользователя Contact в слое Model — вместо этого они получают данные(представленные структурой ContactViewModel) в том виде, в котором они будут отображаться, уже форматированные с помощью ViewModelController.

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

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

import UIKit

class ContactsTableViewCell: UITableViewCell {
    var cellModel: ContactViewModel? {
        didSet {
            bindViewModel()
        }
    }

    func bindViewModel() {
        textLabel?.text = cellModel?.fullName
    }
}


А так класс AddContactViewController:

import UIKit

class AddContactViewController: UIViewController {

    @IBOutlet var firstNameTextField: UITextField!
    @IBOutlet var lastNameTextField: UITextField!
    var contactsViewModelController: ContactViewModelController?
    var didAddContact: ((ContactViewModel, Int) -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        firstNameTextField.becomeFirstResponder()
    }

    @IBAction func didClickOnDoneButton(_ sender: UIBarButtonItem) {
        guard let firstName = firstNameTextField.text,
            let lastName = lastNameTextField.text
            else {
                return
        }

        if firstName.isEmpty || lastName.isEmpty {
            showEmptyNameAlert()
            return
        }

        dismiss(animated: true) { [unowned self] in
            self.contactsViewModelController?.createContact(firstName: firstName, lastName: lastName,
                                                            success: self.didAddContact, failure: nil)
        }

    }

    @IBAction func didClickOnCancelButton(_ sender: UIBarButtonItem) {
        dismiss(animated: true, completion: nil)
    }

    fileprivate func showEmptyNameAlert() {
        showMessage(title: "Error", message: "A contact must have first and last names")
    }

    fileprivate func showMessage(title: String, message: String) {
        let alertView = UIAlertController(title: title,
                                          message: message,
                                          preferredStyle: .alert)
        alertView.addAction(UIAlertAction(title: "Ok", style: .destructive, handler: nil))
        present(alertView, animated: true, completion: nil)
    }

}


И еще раз, в основном здесь идет работа с UI. Заметьте, что AddContactViewController делегирует функционал создания контакта в ViewModelController в функции didClickOnDoneButton(:).

VIEW MODEL

Пришло время поговорить об абсолютно новом для нас слое ViewModel. Для начала создадим класс контакта ContactViewModel, который обеспечит необходимый нам для отображения вид, также будут определены функции < и > с параметрами, для сортировки контактов:

public struct ContactViewModel {
    var fullName: String
}

public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool {
    return lhs.fullName.lowercased() < rhs.fullName.lowercased()
}

public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool {
    return lhs.fullName.lowercased() > rhs.fullName.lowercased()
}


Код ContactViewModelController будет выглядеть следующим образом:

class ContactViewModelController {

    fileprivate var contactViewModelList: [ContactViewModel] = []
    fileprivate var dataManager = ContactLocalDataManager()

    var contactsCount: Int {
        return contactViewModelList.count
    }

    func retrieveContacts(_ success: (() -> Void)?, failure: (() -> Void)?) {
        do {
            let contacts = try dataManager.retrieveContactList()
            contactViewModelList = contacts.map() { ContactViewModel(fullName: $0.fullName) }
            success?()
        } catch {
            failure?()
        }
    }

    func viewModel(at index: Int) -> ContactViewModel {
        return contactViewModelList[index]
    }

    func createContact(firstName: String, lastName: String,
                                 success: ((ContactViewModel, Int) -> Void)?,
                                 failure: (() -> Void)?) {
        do {
            let contact = try dataManager.createContact(firstName: firstName, lastName: lastName)
            let contactViewModel = ContactViewModel(fullName: contact.fullName)
            let insertionIndex = contactViewModelList.insertionIndex(of: contactViewModel) { $0 < $1 }
            contactViewModelList.insert(contactViewModel, at: insertionIndex)
            success?(contactViewModel, insertionIndex)
        } catch {
            failure?()
        }
    }

}


Заметка: MVVM не дает точного определения как создать ViewModel. Когда я хочу создать более многоуровневую архитектуру, я предпочитаю создавать ViewModelController, который будет взаимодействовать со слоем Model и будет отвечать за создание объектов ViewModel.

Главная вещь, которую очень просто запомнить: слой ViewModel не должен участвовать в работе с интерфейсом пользователя. Для того чтобы гарантированно этого избежать лучше просто никогда не импортируйте UIKit в файл c ViewModel.

Класс ContactViewModelController запрашивает контакты из локального хранилища и старается не затрагивать слой Model. Он возвращает данные в формате, который требует view для отображения, и уведомляет view, когда добавляется новый контакт и происходит изменение данных.

В рельной жизни это был бы сетевой запрос, а не запрос к локальной базе, но ни в том ни в другом случае никто из них не должен являться частью ViewModel — и работа с сетью и работа с локальной базой должны обеспечиваться с помощью своих собственных менеджеров(managers).

Вот и все про MVVM. Возможно данный подход покажется вам более тестируемым, поддерживаемым и распределенным чем MVC. А теперь давайте поговорим о VIPER и посмотрим чем он отличается от MVVM.

VIPER

Как это работает:

VIPER является реализацией Clean Architecture для iOS-проектов. Его структура состоит из: View, Interactor, Presenter, Entity, и Router. Это действительно очень распределенная и модульная архитектура, которая позволяет разделить ответственность, очень хорошо покрывается unit-тестами и делает ваш код переиспользуемым.

  • View: Интерфейсный слой, который обычно подразумевает файлы UIKit(включая UIViewController). Вполне понятно, что в более распределенных системах подклассы UIViewController должны относиться к View. В VIPER дела обстоят почти так же как и в MVVM: View отвечает за отображение того что дает Presenter и за передачу введенной пользователем информации или действия в Presenter.
  • Interactor: Содержит бизнес логику, необходимую для работы приложения. Interactor отвечает за получение данных из Model(сетевые или локальные запросы) и его реализация никак не связана с интерфейсом пользователя. Важно помнить, что сетевой и локальный менеджеры не являются частью VIPER, а рассматриваются как отдельные зависимости.
  • Presenter: Отвечает за форматирование данных для отображения во View. В MVVM в нашем примере за это отвечал ViewModelController. Presenter получает данные из Interactor, создает экземпляр ViewModel(форматированный класс, для корректного отображения) и передает его в View. Также он реагирует на ввод пользователем данных, запрашивает дополнительные данные из базы, либо наоборот, передает их ей.
  • Entity: Берет на себя часть ответственность слоя Model, который используется в других архитектурах. Entity это простые объекты данных, без бизнес логики, управляющиеся инетрактором и различными data-менеджерами.
  • Router: Вся навигационная логика приложения. Может показаться что это не самый важный слой, но если вам надо, например, переиспользовать одни и те же вью и на iPhone и на приложении для iPad, единственное что может у вас поменяться это то как будут появляться на экране ваши вью. Это позволяет не трогать больше никакие слои кроме Router, который будет отвечать за это в каждом случае.


В сравнении с MVVM, у VIPER есть несколько ключевых отличий в распределении ответсвенности:

— у него есть Router, отдельный слой ответственный за навигацию

— Entities являются простыми объектами данных, перераспределяя тем самым ответственность за доступ к данным с Model на Interactor

— обязанности ViewModelController распределяются между Interactor и Presenter

А теперь давайте повторим то же приложение, но уже на VIPER. Но для простоты понимания мы сделаем только контроллер с контактами. Код для контроллера добавления нового контакта вы можете найти в проекте по ссылке (папка VIPER Contacts Starter в данном репозитории).

Заметка: Если вы решили делать ваш проект на VIPER, то не стоит пытаться создавать все файлы вручную — можно использовать один из кодогенераторов, например таких как VIPER Gen или Generamba(проект Рамблер).

VIPER Contacts App

VIEW

VIEW представлен элементами из Main.storyboard и классом ContactListView. VIEW очень пассивен; единственные его задачи это передавать интерфейсные события в Presenter и обновлять свое состояние, по уведомлению от Presenter. Вот так выглядит код ContactListView:

import UIKit

class ContactListView: UIViewController {

    @IBOutlet var tableView: UITableView!
    var presenter: ContactListPresenterProtocol?
    var contactList: [ContactViewModel] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter?.viewDidLoad()

        tableView.tableFooterView = UIView()
    }

    @IBAction func didClickOnAddButton(_ sender: UIBarButtonItem) {
        presenter?.addNewContact(from: self)
    }

}

extension ContactListView: ContactListViewProtocol {

    func reloadInterface(with contacts: [ContactViewModel]) {
        contactList = contacts
        tableView.reloadData()
    }

    func didInsertContact(_ contact: ContactViewModel) {
        let insertionIndex = contactList.insertionIndex(of: contact) { $0 < $1 }
        contactList.insert(contact, at: insertionIndex)

        let indexPath = IndexPath(row: insertionIndex, section: 0)
        tableView.beginUpdates()
        tableView.insertRows(at: [indexPath], with: .right)
        tableView.endUpdates()
    }

}

extension ContactListView: UITableViewDataSource {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") else {
            return UITableViewCell()
        }
        cell.textLabel?.text = contactList[(indexPath as NSIndexPath).row].fullName
        return cell
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return contactList.count
    }

}


View шлет в Presenter события viewDidLoad и didClickOnAddButton. По первому событию Presenter запросит данные у Interactor, а по второму Presenter запросит у Router переход на контроллер добавления нового контакта.

Методы протокола ContactListViewProtocol вызываются из Presenter либо когда сделан запрос списка контактов, либо когда добавлен новый контакт. И в том и в другом случае данные во View содержат только ту информацию, которая необходима для отображения.

Также во View находится методы, реализующие протокол UITableViewDataSource, заполняющие таблицу полученными данными.

INTERACTOR

Interactor в нашем примере довольно простой. Все что он делает это запрашивает данные через менеджер локальной базы, и ему абсолютно не важно что использует данный менеджер, CoreData, Realm или какое-либо другое решение. Код в ContactListInteractor будет следующим:

class ContactListInteractor: ContactListInteractorInputProtocol {
    weak var presenter: ContactListInteractorOutputProtocol?
    var localDatamanager: ContactListLocalDataManagerInputProtocol?

    func retrieveContacts() {
        do {
            if let contactList = try localDatamanager?.retrieveContactList() {
                presenter?.didRetrieveContacts(contactList)
            } else {
                presenter?.didRetrieveContacts([])
            }

        } catch {
            presenter?.didRetrieveContacts([])
        }
    }

}


После того как Interactor получает запрошенные данные, он уведомляет Presenter. Еще, как вариант Interactor может передавать ошибку в Presenter, который затем должен будет форматировать ошибку в вид, пригодный для отображения в View.

Заметка: Как вы могли заметить, каждый слой в VIPER реализует протокол. В итоге классы зависят от абстракций, а не от определенной реализации, соответствуя таким образом принципу инверсии зависимостей(один из принципов SOLID).

PRESENTER

Самый главный элемент архитектуры. Все общение между View и остальными слоями(Interactor и Router) проходит через Presenter. Код ContactListPresenter:

class ContactListPresenter: ContactListPresenterProtocol {
    weak var view: ContactListViewProtocol?
    var interactor: ContactListInteractorInputProtocol?
    var wireFrame: ContactListWireFrameProtocol?

    func viewDidLoad() {
        interactor?.retrieveContacts()
    }

    func addNewContact(from view: ContactListViewProtocol) {
        wireFrame?.presentAddContactScreen(from: view)
    }

}

extension ContactListPresenter: ContactListInteractorOutputProtocol {

    func didRetrieveContacts(_ contacts: [Contact]) {
        view?.reloadInterface(with: contacts.map() {
            return ContactViewModel(fullName: $0.fullName)
        })
    }

}

extension ContactListPresenter: AddModuleDelegate {

    func didAddContact(_ contact: Contact) {
        let contactViewModel = ContactViewModel(fullName: contact.fullName)
        view?.didInsertContact(contactViewModel)
    }

    func didCancelAddContact() {}

}


После того как View прогружается, он уведомляет Presenter, который в свою очередь запрашивает данные через Interactor. Когда пользователь нажимает кнопку добавления нового контакта, View уведомляет об этом Presenter, который направляет запрос на открытие экрана добавления нового контакта в Router.

Также Presenter форматирует данные и возвращает их во View после того как выполняется запрос списка контактов. Он также отвечает за реализацию протокола AddModuleDelegate. Это значит, что Presenter получит уведомление, когда будет добавлен новый контакт, подготовит данные контакта для отображения и передаст во View.

Как вы могли заметить, у Presenter есть все шансы стать довольно громоздким. Если есть такая вероятность, то Presenter можно разбить на две части: Presenter, который только получает данные, форматирует их для отображения и передает во View; и на обработчик событий, который будет реагировать на действия пользователя.

ENTITY

Этот слой аналогичен слою Model в MVVM. В нашем приложении он представлен классом Contact и функциями определения операторов < и >. Содержимое Contact будет выглядеть следующим образом:

import CoreData

open class Contact: NSManagedObject {

    var fullName: String {
        get {
            var name = ""
            if let firstName = firstName {
                name += firstName
            }
            if let lastName = lastName {
                name += " " + lastName
            }
            return name
        }
    }

}

public struct ContactViewModel {
    var fullName = ""
}

public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool {
    return lhs.fullName.lowercased() < rhs.fullName.lowercased()
}

public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool {
    return lhs.fullName.lowercased() > rhs.fullName.lowercased()
}


ContactViewModel содержит поля, которые заполняет(форматирует) Presenter, и которые отображает View. Класс Contact является подклассом NSManagedObject, содержащий те же самые поля, что и в модели CoreData.

ROUTER

И наконец последний, но точно не по важности, слой. Вся ответственность за навигацию ложится на Presenter и WireFrame. Presenter получает событие от пользователя и знает когда надо совершить переход, а WireFrame знает как и куда совершить этот переход. Чтобы вы не путались, в данном примере слой Router представлен классом ContactListWireFrame и в тексте упоминается как WireFrame. Код ContactListWireFrame:

import UIKit

class ContactListWireFrame: ContactListWireFrameProtocol {

    class func createContactListModule() -> UIViewController {
        let navController = mainStoryboard.instantiateViewController(withIdentifier: "ContactsNavigationController")
        if let view = navController.childViewControllers.first as? ContactListView {
            let presenter: ContactListPresenterProtocol & ContactListInteractorOutputProtocol = ContactListPresenter()
            let interactor: ContactListInteractorInputProtocol = ContactListInteractor()
            let localDataManager: ContactListLocalDataManagerInputProtocol = ContactListLocalDataManager()
            let wireFrame: ContactListWireFrameProtocol = ContactListWireFrame()

            view.presenter = presenter
            presenter.view = view
            presenter.wireFrame = wireFrame
            presenter.interactor = interactor
            interactor.presenter = presenter
            interactor.localDatamanager = localDataManager

            return navController
        }
        return UIViewController()
    }

    static var mainStoryboard: UIStoryboard {
        return UIStoryboard(name: "Main", bundle: Bundle.main)
    }

    func presentAddContactScreen(from view: ContactListViewProtocol) {

        guard let delegate = view.presenter as? AddModuleDelegate else {
            return
        }

        let addContactsView = AddContactWireFrame.createAddContactModule(with: delegate)
        if let sourceView = view as? UIViewController {
            sourceView.present(addContactsView, animated: true, completion: nil)
        }
    }

}


Поскольку WireFrame отвечает за создание модуля, то будет удобно здесь же провести настройку всех зависимостей. Когда требуется открыть другой контроллер, функция, которая открывает новый контроллер, получает в качестве параметра объект, который будет его открывать, и создает новый контроллер с помощью его WireFrame. Также, при создании нового контроллера ему передаются необходимые данные, в данном случае только делегат(Presenter контроллера с контактами) для получения созданного контакта.

Слой Router дает хорошую возможность избежать использования segues(переходов) в сторибордах и организовать всю навигацию кодом. Так как сториборды не предоставляют компактного решения для передачи данных между контроллерами, наша реализация навигации не добавит лишнего кода. Все что мы получим — это только лучшую прееиспользуемость.


Итоги:

Вы можете найти оба проекта в этом репозитории.

Как вы видите, MVVM и VIPER хоть и отличаются, но не являются уникальными. MVVM говорит нам, что кроме View и Model должен быть еще слой ViewModel. Но ничего не говорится ни о том как должен создаваться этот слой, ни о том как запрашиваются данные — ответственность для этого слоя не определена четко. Существует большое количество способов его реализации и вы можете использовать любой из них.

С другой стороны VIPER является довольно уникальной архитектурой. Он состоит из множества слоев, каждый из которых имеет вполне определенную зону ответственности и менее чем MVVM подвержен влиянию разработчика.

Когда дело доходит до выбора архитектуры, то единственно верного решения обычно нет, но все же я постараюсь дать пару советов. Если у вас большой и длительный проект, с четкими требованиями и вы хотите иметь широкие возможности переиспользования компонентов, то VIPER будет лучшим решением. Более четкое разграничение ответственности дает возможность лучше организовать тестирование и улучшить переиспользуемость.
А какие архитектуры чаще всего используете в своих проектах вы?
41.3% MVC 36
16% MVP 14
29.8% MVVM 26
12.6% MVVM + RxSwift 11
16% Viper 14
17.2% кастомный Clean Architecture 15
9.1% там есть файл, ViewController, обычно пишу все там 8
Voted 87 users. Passed 21 user.
+9
4.9k 75
Comments 2
Top of the day