Anpassung Ihrer vorhandenen Geschäftslösung an SwiftUI. Teil 3. Arbeiten mit Architektur

Guten Tag allerseits! Mit Ihnen, ich, Anna Zharkova, eine führende mobile Entwicklerin von Usetech. Wir

zerlegen weiterhin die Feinheiten von SwiftUI. Die vorherigen Teile finden Sie unter den folgenden Links:

Teil 1
Teil 2

Heute werden wir über die Funktionen der Architektur und das Übertragen und Integrieren vorhandener Geschäftslogik in die SwiftUI-Anwendung sprechen.

Der Standarddatenstrom in SwiftUI basiert auf der Interaktion von View und einem bestimmten Modell, das Eigenschaften und Statusvariablen enthält oder eine solche Statusvariable ist. Daher ist es logisch, dass MVVM der empfohlene Architekturpartner für SwiftUI-Anwendungen ist. Apple schlägt vor, es in Verbindung mit dem Combine-Framework zu verwenden, das deklaratives Api SwiftUI für die Verarbeitung von Werten im Laufe der Zeit einführt. ViewModel implementiert das ObservableObject-Protokoll und stellt als ObservedObject eine Verbindung zu einer bestimmten Ansicht her.



Änderbare Modelleigenschaften werden als @Published deklariert.

class NewsItemModel: ObservableObject {
    @Published var title: String = ""
    @Published var description: String = ""
    @Published var image: String = ""
    @Published var dateFormatted: String = ""
}

Wie bei der klassischen MVVM kommuniziert ViewModel mit dem Datenmodell (d. H. Geschäftslogik) und überträgt die Daten in der einen oder anderen Form.

struct NewsItemContentView: View {
    @ObservedObject var moder: NewsItemModel
    
    init(model: NewsItemModel) {
        self.model = model 
    }
    //... - 
}

MVVM neigt wie fast jedes andere Muster zu Überlastung und
Redundanz. Das überladene ViewModel hängt immer davon ab, wie gut die Geschäftslogik hervorgehoben und abstrahiert ist. Die Last der Ansicht wird durch die Komplexität der Abhängigkeit der Elemente von Zustandsvariablen und Übergängen zu anderen Ansichten bestimmt.

In SwiftUI wird hinzugefügt, dass View eine Struktur und keine Klasse ist und daher keine Vererbung unterstützt, wodurch die Codeduplizierung erzwungen wird .
Wenn dies in kleinen Anwendungen nicht kritisch ist, wird mit zunehmender Funktionalität und Komplexität der Logik eine Überlastung kritisch und eine große Menge an Copy-Paste wird verhindert.

Versuchen wir in diesem Fall, den Ansatz von sauberem Code und sauberer Architektur zu verwenden. Wir können MVVM nicht vollständig aufgeben, schließlich basiert DataFlow SwiftUI darauf, aber es ist einiges zu neu zu erstellen.

Warnung!

Wenn Sie allergisch gegen Artikel über Architektur sind und Clean Code von einem Satz auf den Kopf gestellt wird, scrollen Sie ein paar Absätze nach unten.
Dies ist nicht gerade Clean Code von Onkel Bob!


Ja, wir werden Onkel Bobs Clean Code nicht in seiner reinsten Form annehmen. Ich habe ein Über-Engineering. Wir werden nur eine Idee nehmen.

Die Hauptidee von sauberem Code besteht darin, den am besten lesbaren Code zu erstellen, der dann problemlos erweitert und geändert werden kann.

Es gibt einige Prinzipien für die Softwareentwicklung, deren Einhaltung empfohlen wird.



Viele Menschen kennen sie, aber nicht jeder liebt sie und nicht jeder benutzt sie. Dies ist ein separates Thema für holivar.

Um die Reinheit des Codes sicherzustellen, ist es zumindest erforderlich, den Code in funktionale Schichten und Module zu unterteilen, die allgemeine Problemlösung zu verwenden und die Abstraktion der Interaktion zwischen den Komponenten zu implementieren. Und zumindest müssen Sie den UI-Code von der sogenannten Geschäftslogik trennen.

Unabhängig vom ausgewählten Architekturmuster ist die Logik der Arbeit mit der Datenbank und des Netzwerks, der Verarbeitung und Speicherung von Daten von der Benutzeroberfläche und den Modulen der Anwendung selbst getrennt. Gleichzeitig arbeiten die Module mit Implementierungen von Diensten oder Speichern, die wiederum auf den allgemeinen Dienst von Netzwerkanforderungen oder die allgemeine Datenspeicherung zugreifen. Die Initialisierung von Variablen, über die Sie auf den einen oder anderen Dienst zugreifen können, erfolgt in einem bestimmten allgemeinen Container, auf den das Anwendungsmodul (Modulgeschäftslogik) schließlich zugreift.



Wenn wir Geschäftslogik ausgewählt und abstrahiert haben, können wir die Interaktion zwischen den Komponenten der Module nach Belieben arrangieren.

Grundsätzlich arbeiten alle vorhandenen Muster von iOS-Anwendungen nach dem gleichen Prinzip.



Es gibt immer Geschäftslogik, es gibt Daten. Es gibt auch einen Anrufmanager, der für die Präsentation und Transformation von Daten für die Ausgabe verantwortlich ist und wo die konvertierten Daten ausgegeben werden. Der einzige Unterschied besteht darin, wie die Rollen auf die Komponenten verteilt sind.


weil Wir bemühen uns, die Anwendung lesbar zu machen, um aktuelle und zukünftige Änderungen zu vereinfachen. Es ist logisch, alle diese Rollen zu trennen. Unsere Geschäftslogik wurde bereits hervorgehoben, die Daten werden immer getrennt. Bleiben Sie Dispatcher, Moderator und Ansicht. Als Ergebnis erhalten wir eine Architektur bestehend aus View-Interactor-Presenter, in der der Interaktor mit den Geschäftslogikdiensten interagiert, der Presenter die Daten konvertiert und sie als eine Art ViewModel in unsere Ansicht gibt. In guter Weise werden Navigation und Konfiguration auch aus der Ansicht in separate Komponenten übernommen.



Wir erhalten die VIP + R-Architektur mit der Aufteilung kontroverser Rollen in verschiedene Komponenten.

Schauen wir uns ein Beispiel an. Wir haben eine kleine News Aggregator-Anwendung
in SwiftUI und MVVM geschrieben.



Die Anwendung verfügt über 3 separate Bildschirme mit eigener Logik, d. H. 3 Module:


  • Nachrichtenlistenmodul;
  • Nachrichtenbildschirmmodul;
  • Nachrichtensuchmodul.

Jedes der Module besteht aus einem ViewModel, das mit der ausgewählten Geschäftslogik interagiert, und einer Ansicht, die anzeigt, was das ViewModel an es sendet.



Wir bemühen uns sicherzustellen, dass ViewModel nur Daten speichert, die zur Anzeige bereit sind. Jetzt beschäftigt er sich sowohl mit dem Zugriff auf Dienste als auch mit der Verarbeitung der Ergebnisse.

Wir übertragen diese Rollen an den Präsentator und Interaktor, die wir für jedes
Modul eingerichtet haben.



Der Interaktor leitet die vom Dienst empfangenen Daten an den Präsentator weiter, der das vorhandene ViewModel, das an die Ansicht gebunden ist, mit vorbereiteten Daten füllt. Grundsätzlich ist im Hinblick auf die Trennung der Geschäftslogik des Moduls alles einfach. 


Gehen Sie nun zu Ansicht. Versuchen wir, mit erzwungener Codeduplizierung umzugehen. Wenn es sich um eine Art Kontrolle handelt, kann es sich um seine Stile oder Einstellungen handeln. Wenn wir über die Bildschirmansicht sprechen, dann ist dies:

  • Bildschirmstile
  • allgemeine UI-Elemente (LoadingView);
  • Informationswarnungen;
  • einige allgemeine Methoden.

Wir können keine Vererbung verwenden, aber wir können durchaus Komposition verwenden . Nach diesem Prinzip werden alle benutzerdefinierten Ansichten in SwiftUI erstellt.

Also erstellen wir einen Ansichtscontainer, in den wir dieselbe Logik übertragen, und übergeben unsere Bildschirmansicht an den Containerinitialisierer und verwenden sie dann als Inhaltsansicht innerhalb des Körpers.

struct ContainerView<Content>: IContainer, View where Content: View {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
    }

    var body : some View {
        ZStack {
            content
            if (self.containerModel.isLoading) {
                LoaderView()
            }
        }.alert(isPresented: $containerModel.hasError){
            Alert(title: Text(""), message: Text(containerModel.errorText),
                 dismissButton: .default(Text("OK")){
                self.containerModel.errorShown()
                })
        }
    }

Die Bildschirmansicht ist in den ZStack im ContainerView eingebettet, der auch Code zum Anzeigen der LoadingView und Code zum Anzeigen einer Informationswarnung enthält.

Wir benötigen auch unser ContainerView, um ein Signal vom ViewModel der internen Ansicht zu empfangen und seinen Status zu aktualisieren. Wir können nicht über @Observed dasselbe Modell
wie die interne Ansicht abonnieren , da wir deren Signale ziehen.



Daher stellen wir über das Delegatenmuster eine Kommunikation mit ihm her und verwenden für den aktuellen Status des Containers ein eigenes ContainerModel.

class ContainerModel:ObservableObject {
    @Published var hasError: Bool = false
    @Published var errorText: String = ""
    @Published var isLoading: Bool = false
    
    func setupError(error: String){
     //....
       }
    
    func errorShown() {
     //...
    }
    
    func showLoading() {
        self.isLoading = true
    }
    
    func hideLoading() {
        self.isLoading = false
    }
}

ContainerView implementiert das IContainer-Protokoll. Dem eingebetteten View-Modell wird eine Instanzreferenz zugewiesen.

protocol  IContainer {
    func showError(error: String)
    
    func showLoading()
    
    func hideLoading()
}

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
        self.content.viewModel?.listener = self
    }
    //- 
}

View implementiert das IModelView-Protokoll, um den Modellzugriff zu kapseln und eine Logik zu vereinheitlichen. Modelle für denselben Zweck implementieren das IModel-Protokoll:

protocol IModelView {
    var viewModel: IModel? {get}
}

protocol  IModel:class {
   //....
    var listener:IContainer? {get set}
}

Dann wird bereits in diesem Modell bei Bedarf die Delegate-Methode aufgerufen, um beispielsweise eine Warnung mit einem Fehler anzuzeigen, bei der sich die Statusvariable des Containermodells ändert.

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    //- 
    func showError(error: String) {
        self.containerModel.setupError(error: error)
    }
    
    func showLoading() {
        self.containerModel.showLoading()
    }
    
    func hideLoading() {
        self.containerModel.hideLoading()
    }
}

Jetzt können wir die Arbeit der Ansicht vereinheitlichen, indem wir zur Arbeit über die ContainerView wechseln.
Dies wird unser Leben bei der Arbeit mit der Konfiguration der folgenden Module und der Navigation erheblich erleichtern.
Wie Sie die Navigation in SwiftUI konfigurieren und eine saubere Konfiguration vornehmen, erfahren Sie im nächsten Teil .

Den Quellcode des Beispiels finden Sie hier .

All Articles