Szukaj…
MVVM bez programowania reaktywnego
Zacznę od bardzo krótkiego wyjaśnienia, co to jest i dlaczego warto używać wzorca projektowego Model-View-ViewModel (MVVM) w aplikacjach na iOS. Kiedy iOS pojawił się po raz pierwszy, Apple zaproponował użycie MVC (Model-View-Controller) jako wzorca projektowego. Pokazali to we wszystkich swoich przykładach i wszyscy pierwsi programiści byli zadowoleni z jego używania, ponieważ ładnie oddzieliło obawy między logiką biznesową a interfejsem użytkownika. Gdy aplikacje stały się większe i bardziej złożone, pojawił się nowy problem, odpowiednio nazwany Massive View Controllers (MVC). Ponieważ cała logika biznesowa została dodana do ViewController, z czasem stały się one zwykle zbyt duże i złożone. Aby uniknąć problemu z MVC, w świecie iOS wprowadzono nowy wzorzec projektowy - wzorzec Model-View-ViewModel (MVVM).
Powyższy schemat pokazuje, jak wygląda MVVM. Masz standardowy ViewController + View (w serii ujęć, XIB lub Code), który działa jako widok MVVM (w późniejszym tekście - View będzie odwoływał się do widoku MVVM). Widok ma odniesienie do ViewModel, gdzie znajduje się nasza logika biznesowa. Ważne jest, aby zauważyć, że ViewModel nie wie nic o Widoku i nigdy nie ma odniesienia do widoku. ViewModel ma odniesienie do modelu.
To wystarczy z teoretyczną częścią MVVM. Więcej na ten temat można przeczytać tutaj .
Jednym z głównych problemów z MVVM jest sposób aktualizacji View za pomocą ViewModel, gdy ViewModel nie ma żadnych odniesień i nawet nie wie nic o View.
Główną częścią tego przykładu jest pokazanie, jak korzystać z MVVM (a dokładniej, jak powiązać ViewModel i View) bez żadnego reaktywnego programowania (ReactiveCocoa, ReactiveSwift lub RxSwif). Uwaga: jeśli chcesz korzystać z programowania reaktywnego, nawet lepiej, ponieważ powiązania MVVM są bardzo łatwe w użyciu. Ale ten przykład dotyczy sposobu korzystania z MVVM bez programowania reaktywnego.
Stwórzmy prosty przykład pokazujący, jak korzystać z MVVM.
Nasz MVVMExampleViewController
to prosty ViewController z etykietą i przyciskiem. Po naciśnięciu przycisku tekst etykiety powinien być ustawiony na „Cześć”. Ponieważ podejmowanie decyzji dotyczących interakcji użytkownika jest częścią logiki biznesowej, ViewModel będzie musiał zdecydować, co zrobić, gdy użytkownik naciśnie przycisk. Widok MVVM nie powinien robić żadnej logiki biznesowej.
class MVVMExampleViewController: UIViewController {
@IBOutlet weak var helloLabel: UILabel!
var viewModel: MVVMExampleViewModel?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func sayHelloButtonPressed(_ sender: UIButton) {
viewModel?.userTriggeredSayHelloButton()
}
}
MVVMExampleViewModel
to prosty ViewModel.
class MVVMExampleViewModel {
func userTriggeredSayHelloButton() {
// How to update View's label when there is no reference to the View??
}
}
Można się zastanawiać, jak ustawić odniesienie ViewModel w widoku. Zwykle robię to, gdy ViewController jest inicjowany lub zanim zostanie wyświetlony. W tym prostym przykładzie zrobiłbym coś takiego w AppDelegate
:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if let rootVC = window?.rootViewController as? MVVMExampleViewController {
let viewModel = MVVMExampleViewModel()
rootVC.viewModel = viewModel
}
return true
Prawdziwe pytanie brzmi: jak zaktualizować View z ViewModel bez odniesienia do View do ViewModel? (Pamiętaj, że nie będziemy używać żadnej biblioteki Reactive Programming iOS)
Możesz pomyśleć o użyciu KVO, ale to zbytnio skomplikowałoby sprawę. Niektórzy mądrzy ludzie pomyśleli o tym i wymyślili bibliotekę Bonda . Biblioteka może wydawać się na początku skomplikowana i nieco trudniejsza do zrozumienia, więc wezmę tylko jedną małą jej część i sprawię, że nasz MVVM będzie w pełni funkcjonalny.
Przedstawiamy klasę Dynamic
która jest rdzeniem naszego prostego, ale w pełni funkcjonalnego wzoru MVVM.
class Dynamic<T> {
typealias Listener = (T) -> Void
var listener: Listener?
func bind(_ listener: Listener?) {
self.listener = listener
}
func bindAndFire(_ listener: Listener?) {
self.listener = listener
listener?(value)
}
var value: T {
didSet {
listener?(value)
}
}
init(_ v: T) {
value = v
}
}
Klasa Dynamic
używa generycznych i zamknięć, aby połączyć nasz ViewModel z naszym widokiem. Nie będę wchodził w szczegóły dotyczące tej klasy, możemy to zrobić w komentarzach (aby ten przykład był krótszy). Zaktualizujmy teraz nasz MVVMExampleViewController
i MVVMExampleViewModel
aby korzystać z tych klas.
Nasz zaktualizowany MVVMExampleViewController
class MVVMExampleViewController: UIViewController {
@IBOutlet weak var helloLabel: UILabel!
var viewModel: MVVMExampleViewModel?
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
func bindViewModel() {
if let viewModel = viewModel {
viewModel.helloText.bind({ (helloText) in
DispatchQueue.main.async {
// When value of the helloText Dynamic variable
// is set or changed in the ViewModel, this code will
// be executed
self.helloLabel.text = helloText
}
})
}
}
@IBAction func sayHelloButtonPressed(_ sender: UIButton) {
viewModel?.userTriggeredSayHelloButton()
}
}
Zaktualizowany MVVMExampleViewModel
:
class MVVMExampleViewModel {
// we have to initialize the Dynamic var with the
// data type we want
var helloText = Dynamic("")
func userTriggeredSayHelloButton() {
// Setting the value of the Dynamic variable
// will trigger the closure we defined in the View
helloText.value = "Hello"
}
}
To jest to. Twój ViewModel
może teraz aktualizować View
bez odniesienia do View
.
To naprawdę prosty przykład, ale myślę, że masz pomysł, jak potężny może być. Nie będę wchodził w szczegóły na temat zalet MVVM, ale po przejściu z MVC na MVVM nie wrócisz. Po prostu spróbuj i przekonaj się sam.