In meinem letzten Beitrag in dieser Serie habe ich über das Model-View-Controller-Muster und einige seiner Unvollkommenheiten geschrieben. Trotz der klaren Vorteile, die MVC für die Softwareentwicklung mit sich bringt, verfällt es bei großen oder komplexen Cocoa-Anwendungen.
Dies sind jedoch keine Neuigkeiten. Im Laufe der Jahre haben sich mehrere Architekturmuster herausgebildet, die auf die Mängel des Model-View-Controller-Musters abzielen. Möglicherweise haben Sie davon gehört MVP, Model-View-Presenter und MVVM, Zum Beispiel Model-View-ViewModel. Diese Muster sehen ähnlich aus und fühlen sich ähnlich an wie das Model-View-Controller-Muster, sie behandeln jedoch auch einige der Probleme, die das Model-View-Controller-Muster aufweist.
Ich hatte das Model-View-Controller-Muster bereits seit Jahren verwendet, bevor ich versehentlich auf das Modell gestoßen bin Model-View-ViewModel Muster. Es ist nicht verwunderlich, dass MVVM ein Nachkomme der Cocoa-Community ist, da ihre Ursprünge auf Microsoft zurückgehen. Das MVVM-Muster wurde jedoch auf Cocoa portiert und an die Anforderungen und Bedürfnisse der Cocoa-Gerüste angepasst und hat in der Cocoa-Community in letzter Zeit an Bedeutung gewonnen.
Am attraktivsten ist, wie sich MVVM wie eine verbesserte Version des Model-View-Controller-Musters anfühlt. Dies bedeutet, dass es keine dramatische Änderung der Denkweise erfordert. Sobald Sie die Grundlagen des Musters verstanden haben, ist es ziemlich einfach zu implementieren, nicht schwieriger als das Modell-View-Controller-Muster.
In dem vorherigen Beitrag habe ich geschrieben, dass sich die Controller in einer typischen Cocoa-Anwendung ein wenig von den Controllern unterscheiden, die Reenskaug im ursprünglichen MVC-Muster definiert hat. Unter iOS steuert beispielsweise ein Ansichtscontroller eine Ansicht. Ihre alleinige Verantwortung besteht darin, die von ihm verwaltete Ansicht zu füllen und auf Benutzerinteraktion zu reagieren. Dies ist jedoch nicht die einzige Aufgabe von View-Controllern in den meisten iOS-Anwendungen?
Das MVVM-Pattern führt eine vierte Komponente in die Mischung ein, die Modell anzeigen, was hilft, den View-Controller neu zu fokussieren. Dies geschieht durch die Übernahme einiger Verantwortlichkeiten des View-Controllers. Sehen Sie sich das folgende Diagramm an, um besser zu verstehen, wie das Ansichtsmodell in das Model-View-ViewModel-Muster passt.
Wie das Diagramm zeigt, besitzt das View-Controller das Modell nicht mehr. Das Ansichtsmodell besitzt das Modell, und der Ansichts-Controller fragt das Ansichtsmodell nach den Daten, die angezeigt werden sollen.
Dies ist ein wichtiger Unterschied zum Model-View-Controller-Muster. Der View Controller hat keinen direkten Zugriff auf das Modell. Das Ansichtsmodell übergibt dem Ansichtscontroller die Daten, die er in seiner Ansicht anzeigen muss.
Die Beziehung zwischen dem View Controller und seiner Ansicht bleibt unverändert. Dies ist wichtig, da der View-Controller sich darauf konzentrieren kann, seine Ansicht zu füllen und die Benutzerinteraktion zu handhaben. Dafür wurde der View Controller entwickelt.
Das Ergebnis ist ziemlich dramatisch. Der View-Controller wird auf Diät gesetzt und viele Verantwortlichkeiten werden auf das View-Modell verlagert. Am Ende steht kein View Controller mehr, der Hunderte oder sogar Tausende von Codezeilen umfasst.
Sie fragen sich wahrscheinlich, wie das Ansichtsmodell in das Gesamtbild passt. Was sind die Aufgaben des Sichtmodells? Wie ist das Verhältnis zum View Controller? Und was ist mit dem Modell??
Das Diagramm, das ich Ihnen zuvor gezeigt habe, gibt uns einige Hinweise. Beginnen wir mit dem Modell. Das Modell gehört nicht mehr dem View Controller. Das Ansichtsmodell besitzt das Modell und fungiert als Proxy für den Ansichts-Controller. Wenn der View-Controller ein Datenelement von seinem View-Modell benötigt, fragt dieses sein Modell nach den Rohdaten und formatiert sie so, dass der View-Controller sie sofort in seiner Ansicht verwenden kann. Der View Controller ist nicht für die Datenmanipulation und -formatierung verantwortlich.
Das Diagramm zeigt auch, dass das Modell dem Ansichtsmodell und nicht dem Ansichtscontroller gehört. Es muss auch darauf hingewiesen werden, dass das Model-View-ViewModel-Muster die enge Beziehung zwischen dem View-Controller und seiner Ansicht berücksichtigt, was für Cocoa-Anwendungen charakteristisch ist. Aus diesem Grund fühlt sich MVVM wie selbstverständlich für Cocoa-Anwendungen an.
Da das Model-View-ViewModel-Muster nicht in Cocoa enthalten ist, gibt es keine strengen Regeln für die Implementierung des Musters. Leider wird dies von vielen Entwicklern verwirrt. Um einige Dinge zu klären, möchte ich Ihnen ein grundlegendes Beispiel einer Anwendung zeigen, die das MVVM-Muster verwendet. Wir erstellen eine sehr einfache Anwendung, die Wetterdaten für einen vordefinierten Ort aus der Dark Sky API abruft und dem Benutzer die aktuelle Temperatur anzeigt.
Starten Sie Xcode und erstellen Sie ein neues Projekt auf der Basis von Einzelansicht-Anwendung Vorlage. Ich verwende Xcode 8 und Swift 3 für dieses Tutorial.
Nennen Sie das Projekt MVVM, und setzen Sprache zu Schnell und Geräte zu iPhone.
In einer typischen Cocoa-Anwendung, die mit dem Model-View-Controller-Muster betrieben wird, würde der View-Controller die Netzwerkanforderung durchführen. Sie können einen Manager für die Durchführung der Netzwerkanforderung verwenden, der Ansichtscontroller würde jedoch immer noch wissen, woher die Wetterdaten stammen. Noch wichtiger ist, dass er die Rohdaten erhält und diese formatieren muss, bevor sie dem Benutzer angezeigt werden. Dies ist nicht der Ansatz, den wir verwenden, wenn wir das Model-View-ViewModel-Muster übernehmen.
Lassen Sie uns ein Ansichtsmodell erstellen. Erstellen Sie eine neue Swift-Datei, nennen Sie sie WeatherViewViewModel.swift, und definieren Sie eine Klasse mit dem Namen WeatherViewViewModel
.
Importieren der Foundation-Klasse WeatherViewViewModel
Die Idee ist einfach. Der Ansichts-Controller fragt das Ansichtsmodell nach der aktuellen Temperatur für einen vordefinierten Ort. Da das Ansichtsmodell eine Netzwerkanforderung an die Dark Sky-API sendet, akzeptiert die Methode eine Schließung, die aufgerufen wird, wenn das Ansichtsmodell über Daten für den Ansichts-Controller verfügt. Diese Daten können die aktuelle Temperatur sein, es kann sich jedoch auch um eine Fehlermeldung handeln. Das ist was der StromTemperatur (Fertigstellung :)
Methode des Ansichtsmodells sieht so aus. Wir werden die Details in wenigen Augenblicken ausfüllen.
Import-Foundation-Klasse WeatherViewViewModel // MARK: - Typ Alias typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - Öffentliche API-Funktion fc currentTemperature (Abschluss: @escaping CurrentTemperatureCompletion)
Wir deklarieren einen Typenalias und definieren eine Methode, StromTemperatur (Fertigstellung :)
, das akzeptiert eine Schließung des Typs CurrentTemperatureCompletion
.
Die Implementierung ist nicht schwer, wenn Sie mit dem Networking und dem Internet vertraut sind URLSession
API. Schauen Sie sich den Code unten an und beachten Sie, dass ich eine Aufzählung verwendet habe, API
, um alles schön sauber zu halten.
Import Foundation-Klasse WeatherViewViewModel // MARK: - Typ Alias typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - API-Enum-API statisch lat lat = 37.8267 statisch lang = -122.4233 statisch let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxx" statisch baseURL = URL (Zeichenfolge: "https://api.darksky.net/forecast")! static var requestURL: URL return API.baseURL .appendingPathComponent (API.APIKey) .appendingPathComponent ("\ (lat), \ (long)") // MARK: - Öffentliche API-Funktion currentTemperature (Abschluss: @escaping CurrentTemperatureCompletion) let dataTask = URLSession.shared.dataTask (mit: API.requestURL) [Schwaches Selbst] (Daten, Antwort, Fehler) in // Helpers var formattedTemperature: String? if let data = data formatierteTemperatur = selbst? .temperatur (von: data) DispatchQueue.main.async Completion (formatierte Temperatur? "Wetterdaten können nicht abgerufen werden") // Datenaufgabe dataTask.resume () fortsetzen
Das einzige Stück Code, das ich Ihnen noch nicht gezeigt habe, ist die Implementierung von Temperatur (von :)
Methode. Bei dieser Methode extrahieren wir die aktuelle Temperatur aus der Antwort von Dark Sky.
// MARK: - Helper Methods func Temperatur (aus Daten: Daten) -> String? guard let JSON = versuchen? JSONSerialization.jsonObject (mit: Daten, Optionen: []) als? [String: Beliebig] else return nil guard derzeit = JSON? ["Derzeit"] als? [String: Any] else return nil guard Temperatur = aktuell ["Temperatur"] als? Double else return nil return Zeichenfolge (Format: "% .0f ° F", Temperatur)
In einer Produktionsanwendung würde ich mich für eine robustere Lösung entscheiden, um die Antwort zu analysieren, z. B. ObjectMapper oder Unbox.
Wir können das Ansichtsmodell jetzt im Ansichtscontroller verwenden. Wir erstellen eine Eigenschaft für das Ansichtsmodell und definieren außerdem drei Ausgänge für die Benutzeroberfläche.
UIKit-Klasse importieren ViewController: UIViewController // MARK: - Eigenschaften @IBOutlet var temperatureLabel: UILabel! // MARK: - @IBOutlet var fetchWeatherDataButton: UIButton! // MARK: - @IBOutlet var activityIndicatorView: UIActivityIndicatorView! // MARK: - private let viewModel = WeatherViewViewModel ()
Beachten Sie, dass der Ansichtscontroller das Ansichtsmodell besitzt. In diesem Beispiel ist der View-Controller auch für die Instantiierung seines View-Modells verantwortlich. Generell ziehe ich es vor, das Ansichtsmodell in den Ansichtscontroller zu injizieren, aber lassen Sie es uns jetzt einfach machen.
In der Ansicht Controller viewDidLoad ()
Methode rufen wir eine Hilfsmethode auf, fetchWeatherData ()
.
// MARK: - View Life Cycle überschreiben func viewDidLoad () super.viewDidLoad () // Wetterdaten abrufen fetchWeatherData ()
Im fetchWeatherData ()
, Wir fragen das Ansichtsmodell nach der aktuellen Temperatur. Bevor wir die Temperatur abfragen, blenden wir die Beschriftung und die Schaltfläche aus und zeigen die Aktivitätsanzeige an. In der Schließung gehen wir weiter fetchWeatherData (Fertigstellung :)
, Wir aktualisieren die Benutzeroberfläche, indem wir die Temperaturbezeichnung auffüllen und die Aktivitätsanzeige ausblenden.
// MARK: - Helper Methods private func fetchWeatherData () // Benutzeroberfläche ausblenden temperatureLabel.isHidden = true fetchWeatherDataButton.isHidden = true // Aktivitätsanzeige anzeigen Anzeige activityIndicatorView.startAnimating () // Abrufen der Wetterdaten viewModel.currentTemperature [nicht gesäumt self] (Temperatur) in // Update Temperature Label self.temperatureLabel.text = Temperatur self.temperatureLabel.isHidden = false // Schaltfläche zum Abrufen der Wetterdaten anzeigen self.fetchWeatherDataButton.isHidden = false // Aktivitätsanzeige ausblenden Ansicht self.activityIndicatorView.stopAnimating ()
Die Schaltfläche ist mit einer Aktion verbunden, fetchWeatherData (_ :)
, in dem wir auch das anrufen fetchWeatherData ()
Hilfsmethode. Wie Sie sehen, hilft uns die Hilfsmethode, Code-Duplizierungen zu vermeiden.
// MARK: - Actions @IBAction func fetchWeatherData (_ sender: Any) // Wetterdaten abrufen fetchWeatherData ()
Der letzte Teil des Puzzles besteht darin, die Benutzeroberfläche der Beispielanwendung zu erstellen. Öffnen Hauptplatine und fügen Sie einer vertikalen Stapelansicht ein Etikett und eine Schaltfläche hinzu. Oben in der Stapelansicht wird eine Aktivitätsanzeige hinzugefügt, die vertikal und horizontal zentriert ist.
Vergessen Sie nicht, die Ausgänge und die von uns definierte Aktion zu verkabeln ViewController
Klasse!
Jetzt erstellen und starten Sie die Anwendung, um es auszuprobieren. Denken Sie daran, dass Sie einen Dark Sky-API-Schlüssel benötigen, damit die Anwendung funktioniert. Sie können sich auf der Dark Sky-Website für ein kostenloses Konto anmelden.
Obwohl wir nur ein paar Kleinigkeiten in das Ansichtsmodell verschoben haben, fragen Sie sich vielleicht, warum dies notwendig ist. Was haben wir gewonnen? Warum sollten Sie diese zusätzliche Komplexitätsebene hinzufügen??
Der offensichtlichste Vorteil ist, dass der View-Controller schlanker ist und sich mehr auf die Verwaltung seiner Ansicht konzentriert. Das ist die Kernaufgabe eines View-Controllers: seine Ansicht verwalten.
Aber es gibt einen subtileren Nutzen. Da der View Controller nicht für das Abrufen der Wetterdaten von der Dark Sky-API verantwortlich ist, sind ihm die Details zu dieser Aufgabe nicht bekannt. Die Wetterdaten können von einem anderen Wetterdienst oder von einer zwischengespeicherten Antwort stammen. Der View Controller würde es nicht wissen und muss es auch nicht wissen.
Das Testen verbessert sich auch erheblich. Es ist bekannt, dass Ansichts-Controller aufgrund ihrer engen Beziehung zur Ansichtsebene schwer zu testen sind. Indem wir einen Teil der Geschäftslogik in das Ansichtsmodell verschieben, verbessern wir sofort die Testbarkeit des Projekts. Das Testen von Ansichtsmodellen ist überraschend einfach, da sie keinen Link zur Ansichtsebene der Anwendung haben.
Das Model-View-ViewModel-Muster ist ein bedeutender Fortschritt beim Entwurf von Cocoa-Anwendungen. Ansichts-Controller sind nicht so umfangreich, Ansichtsmodelle lassen sich einfacher zusammenstellen und testen, und Ihr Projekt wird dadurch leichter handhabbar.
In dieser kurzen Serie haben wir nur die Oberfläche zerkratzt. Es gibt noch viel mehr über das Model-View-ViewModel-Muster zu schreiben. Es ist im Laufe der Jahre zu einem meiner Lieblingsmuster geworden, und deshalb rede und schreibe ich ständig darüber. Probieren Sie es aus und lassen Sie mich wissen, was Sie denken!
In der Zwischenzeit können Sie einige unserer anderen Beiträge über die Entwicklung von Swift und iOS-Apps lesen.