Model-View-ViewModel (MVVM) в iOS разработке

Разберем подробнее шаблон проектирования на языке Swift - Model-View-ViewModel.

Как уже отмечалось ранее MVVM пришел на замену MVC для того чтобы облегчить процесс разработки сложных приложений, позволить отделить логические части проекта на разные объекты, отделить бизнес-логику от пользовательского интерфейса (View) и обложить все это качественным тестированием.

MVVM


Вот что говорит Википедия о MVVM.

MVVM состоит из модели (Model), представления (View) или пользовательского интерфейса, и ViewModel - сущности, которая изолирована от пользовательского интерфейса, которая содержит всю бизнес-логику приложения, которая общается напрямую с моделью данных, и которая как-то должна влиять на View. Model в свою очередь, должна быть максимально простой и прозрачной. Она никак не связывается с представлением, а лишь делает свою работу по обработке данных (загрузка, созранение, изменение). Все входные данные приходят из ViewModel.

Ключевым моментом в создании MVVM является связывание (binding) пользовательского интерфейса (View) с логикой (ViewModel).

Связывание пользовательского интерфейса — это мост между View и ViewModel. Он может быть однонаправленный или двунаправленный и позволяет этим двум сущностям взаимодействовать совершенно прозрачным образом.

К сожалению, в iOS нет собственного способа добиться этого, поэтому вы должны использовать сторонние библиотеки/фреймворки или написать связывание самостоятельно.

Существуют разные способы реализовать привязку пользовательского интерфейса к коду Swift:

  • Delegation
  • Closures
  • RxSwift (ReactiveCocoa)

При помощи Делегирования (Delegation)

Простое объяснения паттерна "Делегирование".

Если вы хотите избежать импорта и изучения новых фреймворков, вы можете использовать делегирование в качестве альтернативного способа для связывания View и ViewModel. К сожалению, используя этот подход, вы теряете силу прозрачной привязки, поскольку вам приходится делать привязку вручную. Эта версия MVVM становится очень похожей на MVP. Стратегия этого подхода заключается в сохранении ссылки на делегата, реализованного представлением, внутри вашей модели представления. Таким образом, ViewModel может обновлять представление, не имея ссылок на объекты UIKit.

// Часть MVVM, отвечающая за реализацию View
class ViewController: UIViewController, ViewModelDelegate {

    @IBOutlet private weak var label: UILabel?

    private let viewModel: ViewModel

    init(viewModel: ViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
        viewModel.delegate = self
    }

    func lableDidChange(text: String) {
        label?.text = text
    }
}

protocol ViewModelDelegate: class {
    func labelDidChange(text: String)
}

// Часть MVVM, отвечающая за реализацию ViewModel
class ViewModel {

    private var labelText: String {
        didSet {
            delegate?.labelDidChange(text: labelText)
        }
    }

    weak var delegate: ViewModelDelegate? {
        didSet {
            delegate?.labelDidChange(text: labelText)
        }
    }

    init() {
        labelText = "Hello World!"
    }
}

При помощи Замыканий (Closures)

Это очень похоже на делегирование, но вместо делегата вы используете замыкания. Замыкания — это свойства ViewModel, и View использует их для обновления пользовательского интерфейса. Вы должны обратить внимание на то, чтобы избежать зацикливаний в замыканиях с использованием [weak self].

class ViewController: UIViewController, ViewModelDelegate {

    @IBOutlet private weak var userLabel: UILabel?

    private let viewModel: ViewModel

    init(viewModel: ViewModel) {
        self.viewModel = viewModel
        viewModel.delegate = self
    }

    func userNameDidChange(text: String) {
        userLabel?.text = text
    }
}

protocol ViewModelDelegate: class {
    func userNameDidChange(text: String)
}

class ViewModel {

    private var userName: String {
        didSet {
            delegate?.userNameDidChange(text: userName)
        }
    }
    weak var delegate: ViewModelDelegate? {
        didSet {
            delegate?.userNameDidChange(text: userName)
        }
    }

    init() {
        userName = "Hello World!"
    }
}

При помощи реактивного программирования и библиотеки RxSwift (ReactiveCocoa)

RxSwift — это Swift-версия семейства фреймворков ReactiveX. Освоив его, вы сможете легко переключаться на любой другой язык программирования где используется реактивное программирование - RxJava, RxJavascript и т. д.

Этот фреймворк позволяет вам писать код в стиле функционального реактивного программирования (FRP), а благодаря внутренней библиотеке RxCocoa вы можете легко связать View и ViewModel:

class ViewController: UIViewController {

    @IBOutlet private weak var userLabel: UILabel!

    private let viewModel: ViewModel
    private let disposeBag: DisposeBag

    private func bindToViewModel() {
        viewModel.myProperty
            .drive(userLabel.rx.text)
            .disposed(by: disposeBag)
    }
}

Существуют еще и другие способы связывания (binding) View и ViewModel, но мы рассмотрели основные. В своем ролике на YouTube я показывал, как можно реализовать связывание View и ViewModel с помощью дженериков и замыканий: