Kombinieren Sie MVVMs in UIKit- und SwiftUI-Anwendungen für UIKit-Entwickler



Wir wissen, dass ObservableObject-Klassen mit ihren @PublishedEigenschaften Combinespeziell für View Modelin erstellt werden SwiftUI. Aber genau das gleiche View Modelkann bei der UIKitImplementierung der Architektur verwendet werden  MVVM, obwohl wir in diesem Fall binddie UIElemente manuell an die @Published Eigenschaften von View Model binden müssen . Sie werden überrascht sein, aber mit Hilfe Combinedieser werden einige Codezeilen erstellt. Wenn UIKitSie sich beim Entwerfen von Anwendungen an diese Ideologie halten , wechseln Sie anschließend problemlos zu SwiftUI.

Der Zweck dieses Artikel ist mit einem primitiv einfachen Beispiel zu zeigen , wie man elegant eine implementieren MVVMArchitektur UIKitmit  Combine. Im Gegensatz dazu zeigen wir die Verwendung derselbenView Model c SwiftUI.

In diesem Artikel werden zwei einfache Anwendungen erläutert, mit denen Sie die neuesten Wetterinformationen für eine bestimmte Stadt auf der OpenWeatherMa p- Website auswählen können . Aber UIeiner von ihnen wird mit der Anwendung erstellt SwiftUI, der andere mit Hilfe UIKit. Für den Benutzer sehen diese Anwendungen fast gleich aus.



Der Code ist auf Github .

Die Benutzeroberfläche ( UI) enthält nur zwei UI Elemente: ein Textfeld zur Eingabe der Stadt und eine Beschriftung zur Anzeige der Temperatur. Das Textfeld für die Eingabe der Stadt ist das aktive INPUT ( Input), und die Temperaturbezeichnung ist das passive EXIT ( Output).  

Die Rolle View Model in der Architektur MVVMbesteht darin, dass sie die EINGABE (n) von View(oder ViewControllerin UIKit) übernimmt , die Geschäftslogik der Anwendung implementiert und die AUSGABEN an View(oder ViewControllerin UIKit) zurückgibt  , wobei diese Daten möglicherweise im gewünschten Format dargestellt werden.

Erstellen  View Modelmit Combineegal , welche Art von Business - Logik - synchron oder asynchron - sehr einfach , wenn Sie eine verwenden ObservableObject Klasse mit ihren @PublishedEigenschaften.

API OpenWeatherMap


Obwohl Sie mit dem OpenWeatherMap- Dienst    sehr umfangreiche Wetterinformationen auswählen können, ist das Modell der Daten, an denen wir interessiert sind, sehr einfach. Es enthält detaillierte Informationen  WeatherDetailzum aktuellen Wetter in der ausgewählten Stadt und befindet sich in der Datei  Model.swift :



Obwohl wir uns bei dieser speziellen Aufgabe nur für die Temperatur interessieren temp, welche In der Struktur  Mainbietet das Modell ausführliche Informationen zum aktuellen Wetter als Stammstruktur. Sie  WeatherDetailsind davon überzeugt, dass Sie in Zukunft die Funktionen dieser Anwendung erweitern möchten. Die Struktur WeatherDetail ist codierbar. Auf diese Weise können wir die JSONDaten mit nur zwei Codezeilen buchstäblich in das Modell decodieren  .

Die Struktur  WeatherDetail sollte auch seinIdentifiablewenn wir es uns in Zukunft leichter machen wollen, eine Reihe von Wettervorhersagen [WeatherDetail] für mehrere Tage im Voraus in Form einer Liste  List von anzuzeigen  SwiftUI. Dies ist auch ein Leerzeichen für eine zukünftige anspruchsvollere aktuelle Wetter-App. Das Protokoll Identifiableerfordert das Vorhandensein der Eigenschaft id,, die wir bereits haben, so dass keine zusätzlichen Anstrengungen von uns erforderlich sind.

In der Regel bieten Dienste, einschließlich des OpenWeatherMap- Dienstes  , alle Arten von Diensten URLs an, um die von uns benötigten Ressourcen zu erhalten. Der OpenWeatherMap- Service  bietet uns die Möglichkeit, URLsdetaillierte Informationen über das aktuelle Wetter oder die Vorhersage für 5 Tage in einer bestimmten Stadt abzurufen city. In dieser Anwendung interessieren uns nur aktuelle Wetterinformationen und für diesen FallURLberechnet mit der Funktion absoluteURL (city: String):



API Für den OpenWeatherMap- Dienst platzieren  wir ihn in der WeatherAPI.swift- Datei . Sein zentraler Teil wird eine Methode zur Auswahl detaillierter Wetterinformationen  WeatherDetailin einer Stadt sein  city:

  • fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>

Im Rahmen des Frameworks gibt Combine diese Methode nicht nur detaillierte Wetterinformationen zurück  WeatherDetail, sondern auch den entsprechenden "Herausgeber" Publisher. Unser "Herausgeber" AnyPublisher<WeatherDetail, Never>gibt keinen Fehler zurück - Neverund wenn immer noch ein Stichproben- oder Codierungsfehler aufgetreten ist, kehrt der Stellvertreter  WeatherDetail.placeholderohne zusätzliche Meldungen zur Fehlerursache zurück. 

Betrachten Sie die Methode fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>, mit der  detaillierte Wetterinformationen für die Stadt auf der OpenWeatherMap- Website ausgewählt werden  cityund keine Fehler zurückgegeben werden  , genauer Never:



  1. basierend auf dem Namen der Stadt, wir  city bilden URL die Funktion mit absoluteURL(city:city)detaillierten Wetterinformationen anzufordern  WeatherDetail,
  2. «» dataTaskPublisher(for:), Output (data: Data, response: URLResponse),   Failure - URLError,
  3. map { } (data: Data, response: URLResponse)  data
  4. JSON  data ,  WeatherDetail, ,
  5. - «»  catch (error ... )  «» WeatherDetail.placeholder,
  6. main , UI,
  7. «» «» eraseToAnyPublisher() AnyPublisher.

Der so erhaltene asynchrone "Verlag" AnyPublisherhebt nicht von selbst ab, liefert nichts, bis jemand ihn "abonniert". Wir werden es in einer verwenden  ObservableObject Klasse , die eine Rolle spielt View Modelin  SwiftUIbeide und UIKit

Ansichtsmodell erstellen


So View Modelerstellen Sie eine sehr einfache Klasse  TempViewModel, die ein Protokoll ObservableObject mit zwei  @Published Eigenschaften implementiert :  



  1. Eine davon  @Published var city: Stringist die Stadt (Sie können sie bedingt als EINGANG bezeichnen, da ihr Wert vom Benutzer am geregelt wird View).  
  2. Das zweite  @Published var currentWeather = WeatherDetail.placeholder ist das Wetter in dieser Stadt im Moment (wir können diese Eigenschaft unter der Bedingung EXIT nennen, da sie durch Abrufen von Daten von der OpenWeatherMap- Website erhalten wird  ).

Sobald wir eine  @Published Eigenschaft festgelegt haben  city, können wir sie sowohl als einfache Eigenschaft  cityals auch als "Herausgeber" verwenden  $city.

In einer Klasse  TempViewModelkönnen Sie nicht nur die für uns interessanten Eigenschaften deklarieren, sondern auch die Geschäftslogik ihrer Interaktion vorschreiben. Zu diesem Zweck können wir beim Initialisieren einer Instanz der Klasse  TempViewModel in init?ein „Abonnement“ erstellen, das während des gesamten „Lebenszyklus“ der Instanz der Klasse funktioniert  TempViewModelund die Abhängigkeit des aktuellen Wetters currentWeather von der Stadt  reproduziert  city.

Dazu Combinestrecken wir die Kette von der Eingabe "Herausgeber" $city zur Ausgabe "Herausgeber" AnyPublisher<WeatherDetail, Never>, deren Wert das aktuelle Wetter ist. Anschließend „abonnieren“ wir es mit Hilfe eines „Abonnenten“ assign (to: \.currentWeather, on: self) und erhalten Sie den gewünschten Wert des aktuellen Wetters  currentWeather als "Ausgabe"  @Published -Eigenschaft.

Wir müssen die Kette NICHT einfach aus den Eigenschaften ziehen city, nämlich aus den "Verlegern" $city, die an der Erstellung teilnehmen werden, UI und dort werden wir sie ändern.

Wie machen wir das?

Wir haben bereits eine Funktion in unserem Arsenal fetchWeather (for city: String), die sich in der Klasse befindet  WeatherAPIund den "Herausgeber" AnyPublisher<WeatherDetail, Never> mit detaillierten Wetterinformationen je nach Stadt  zurückgibt city, und wir können den Wert des "Herausgebers" nur irgendwie verwenden  $city, um daraus ein Argument dieser Funktion zu machen.

 Gehen Sie zum richtigen Verlag  fetchWeather (for city: String) , Combineum uns zu  helfen: Betreiber  flatMap:



BetreiberflatMaperstellt einen neuen "Herausgeber" basierend auf Daten, die vom vorherigen "Herausgeber" empfangen wurden.

Als nächstes "abonnieren" wir diesen neu empfangenen "Verlag" mit Hilfe eines sehr einfachen "Abonnenten"  assign (to: \.currentWeather, on: self)und weisen der @PublishedEigenschaft  den vom "Verlag" erhaltenen Wert zu currentWeather:



Wir haben gerade einen init( )ASYNCHRONEN "Verlag" erstellt und "abonniert", was zu einem AnyCancellable"Abonnement" führt. ".

AnyCancellable Das "Abonnement" ermöglicht es dem Anrufer, das "Abonnement" jederzeit zu kündigen und keine Werte mehr vom "Herausgeber" zu erhalten. Darüber hinaus  AnyCancellablewird der vom "Herausgeber" belegte Speicher freigegeben , sobald das "Abonnement" seinen Gültigkeitsbereich verlässt. Daher wird init( ) dieses „Abonnement“ vom System gelöscht , sobald es abgeschlossen ist ARCund keine Zeit haben, die asynchronen Informationen über das aktuelle Wetter mit einer Zeitverzögerung zuzuweisen  currentWeather. Um ein solches "Abonnement" zu speichern, muss eine OUTSIDE- init()Variable erstellt var cancellableSetwerden, die unser AnyCancellable"Abonnement" während des gesamten "Lebenszyklus" der Klasseninstanz in dieser Variablen  behält TempViewMode

Das AnyCancellable"Abonnement" in der Variablen wird cancellableSetmit dem Operator  gespeichert store ( in: &self.cancellableSet):



Dadurch bleibt das "Abonnement" während des gesamten "Lebenszyklus" der Klasseninstanz erhalten  TempViewModel. Wir können den Wert des Verlags nach Wunsch ändern $city, und das aktuelle Wetter currentWeather für diese Stadt wird immer zu unserer Verfügung stehen .

Um die Anzahl der Serveranrufe bei der Eingabe einer Stadt zu reduzieren cityWir sollten nicht direkt den „Herausgeber“ der Zeile mit dem Namen der Stadt verwenden  $city, sondern die geänderte Version mit Operatoren debounceund removeDuplicates:



Der Operator wird  debounce verwendet, um zu warten, bis der Benutzer die erforderlichen Informationen auf der Tastatur eingegeben hat, und die ressourcenintensive Aufgabe nur einmal auszuführen.

Ebenso veröffentlicht ein Operator removeDuplicatesWerte nur, wenn sie sich von vorherigen Werten unterscheiden. Wenn der Benutzer zum Beispiel zuerst eintritt john, erhalten wir dann joeund wieder nur einmal. Dies trägt dazu bei, unsere effizienter zu machen.johnjohnUI

Erstellen einer Benutzeroberfläche mit SwiftUI


Jetzt, wo wir es haben View Model, fangen wir an UI. Erst um SwiftUIund dann um UIKit.

In Xcodeerstellen wir ein neues Projekt mit SwiftUIund in der resultierenden Struktur platzieren wir ContentView  unser  View Modelals @ObservedObject Variable model. Durch Text ("Hello, World!") den Titel  ersetzen  Text ("WeatherApp"), ein Textfeld zur Eingabe der Stadt TextField ("City", text: self.$model.city) und eine Beschriftung zur Anzeige der Temperatur hinzufügen  :



Wir haben die Werte unserer Variablen direkt verwendet model: TempViewModel(). Wir haben im Textfeld die Stadt eingegeben $model.cityund im Etikett die Temperatur angezeigt - model.currentWeather.main?.temp.

Änderungen an den  @Published Eigenschaften führen nun zu einem "Neuzeichnen" View:



Dies wird durch die Tatsache sichergestellt, dass dies bei uns der Fall View Model ist@ObservedObjectDas heißt, die automatische Bindung ( binding) @Publishedunserer Eigenschaften View Modelund Elemente der Benutzeroberfläche ( UI) wird ausgeführt . Eine solche AUTOMATISCHE "Bindung" ist nur in möglich SwiftUI.

Erstellen einer Benutzeroberfläche mit UIKit


Was tun damit UIKit? Es ist nicht da  @ObservedObject. In werden  UIKit wir die "Bindung" ( binding) manuell durchführen. Es gibt viele Möglichkeiten, diese „manuelle Bindung“ durchzuführen:

  • Key-Value Observing oder KVO: ein Mechanismus  key pathszum Überwachen einer Eigenschaft und zum Empfangen einer Benachrichtigung, dass sie geändert wurde.
  • Funktionale reaktive Programmierung oder FRP: Verwendung eines Frameworks Combine.
  • Delegation: Verwenden von Delegate-Methoden zum Senden einer Benachrichtigung, dass sich ein Eigenschaftswert geändert hat.
  • Boxing: didSet { } , .

Angesichts des Titels des Artikels werden wir natürlich vor Ort arbeiten Combine. In der UIKitAnwendung zeigen wir, wie einfach es ist, eine „manuelle Bindung“ durchzuführen Combine.

In der UIKitAnwendung haben wir auch zwei UI Elemente: UITextFieldzum Betreten der Stadt und UILabelzum Anzeigen der Temperatur. In werden ViewControllerwir natürlich folgende OutletElemente haben:





In Form einer gewöhnlichen Variablen viewModelhaben wir das gleiche wie View Modelim vorherigen Abschnitt:



Bevor wir die „manuelle Bindung“ mit durchführen Combine, machen wir das Textfeld zu UITextFieldunserem Verbündeten und zum „Herausgeber“ unseres Inhalts text:



Auf diese Weise können wir die viewDidLoadmanuelle Bindung mithilfe der Funktion sehr einfach implementierenbinding ():



Wir „subscribe“ in die „Publisher“ In der Tat cityTextField.textPublishermit Hilfe eines sehr einfachen „Teilnehmer“  assign (to: \.city, on: viewModel)und weisen Sie den Text vom Benutzer in dem Textfeld eingegeben hat, cityTextFieldum unser „input“  @PublishedEigentum von cityuns View Model.

Darüber hinaus nehmen wir Änderungen in eine andere Richtung vor: Wir „abonnieren“ die @PublishedEigenschaft  „Ausgabe“  $currentWeather mit Hilfe des „Teilnehmers“ sink und dessen Schließung receiveValue, bilden den Temperaturwert und weisen ihn dem Etikett zu temperatureLabel.

Im viewDidLoad "Abonnement" empfangen  wird in einer Variablen gespeichert var cancellableSet. Nachdem wir sie einmal erstellt haben, ermöglichen wir ihnen, während des gesamten „Lebenszyklus“ der Klasseninstanz ViewControllerund zusammen mit dem „Abonnement“ in unserer zu handeln View ModelImplementieren Sie die gesamte Geschäftslogik der Anwendung.

Das Protokoll ObservableObjectfunktioniert übrigens UIKitnicht, stört aber nicht. UIKit Das Protokoll ist völlig gleichgültig ObservableObjectund könnte im Prinzip  View Modelin UIKit Anwendungen entfernt werden:



Wir werden dies jedoch nicht tun, da wir es View Modelsowohl für die aktuelle Anwendung als auch für UIKitzukünftige Anwendungen unverändert lassen möchten SwiftUI.

Das ist alles. Der Code ist auf Github .

Fazit

Ein funktionales reaktives Framework Combinemacht es sehr einfach und präzise, ​​die MVVMArchitektur sowohl SwiftUIin UIKitals auch in Form von verständlichem und lesbarem Code zu implementieren .

Links:

Kombinieren Sie + UIKit + MVVM
Verwenden von Combine
iOS MVVM Tutorial: Refactoring von MVC
MVVM mit Combine Tutorial für iOS

PS Wenn Sie einige Wetterinformationen anzeigen möchten, müssen Sie sich bei OpenWeatherMap registrieren  und diese abrufen API key . Dieser Vorgang dauert nicht länger als 2 Minuten.

All Articles