Der richtige Weg, um Status zwischen Swift View Controllern zu teilen

Was Sie erstellen werden

Vor einigen Jahren, als ich noch in einer mobilen Beratung tätig war, arbeitete ich an einer App für eine große Investmentbank. Große Unternehmen, insbesondere Banken, verfügen normalerweise über Prozesse, um sicherzustellen, dass ihre Software sicher, robust und wartbar ist.

Ein Teil dieses Prozesses bestand darin, den Code der von mir geschriebenen App zur Überprüfung an einen Dritten zu senden. Das hat mich nicht gestört, weil ich dachte, mein Code sei einwandfrei und das Review-Unternehmen würde dasselbe sagen.

Als ihre Antwort zurückkam, war das Urteil anders, als ich dachte. Obwohl sie sagten, die Qualität des Codes sei nicht schlecht, wiesen sie darauf hin, dass der Code schwer zu warten und zu testen sei (Gerätetests waren damals bei iOS-Entwicklung nicht sehr beliebt)..

Ich wies ihr Urteil zurück und dachte, mein Kodex sei großartig, und es gab keine Möglichkeit, ihn zu verbessern. Sie dürfen es einfach nicht verstehen!

Ich hatte die typische Entwickler-Hybris: Wir denken oft, dass das, was wir tun, großartig ist und andere es nicht verstehen. 

Im Nachhinein habe ich mich geirrt. Nicht viel später las ich über einige Best Practices. Von da an ragten die Probleme in meinem Code wie ein Daumen auf. Mir wurde klar, dass ich, wie viele iOS-Entwickler, einigen klassischen Fallstricken schlechter Kodierungspraktiken erlegen war.

Was die meisten iOS-Entwickler falsch machen

Eine der häufigsten Vorgehensweisen bei der iOS-Entwicklung tritt auf, wenn der Status zwischen den View-Controllern einer App übertragen wird. Ich selbst bin in die Vergangenheit geraten.

Die Verbreitung von Status über View Controller ist in jeder iOS-App von entscheidender Bedeutung. Wenn Ihre Benutzer durch die Bildschirme Ihrer App navigieren und mit ihr interagieren, müssen Sie einen globalen Status beibehalten, der alle Änderungen aufzeichnet, die der Benutzer an den Daten vornimmt.

Und hier greifen die meisten iOS-Entwickler zur offensichtlichen, aber falschen Lösung: dem Singleton-Pattern.

Das Singleton-Pattern ist besonders in Swift sehr schnell zu implementieren und funktioniert gut. Sie müssen nur eine statische Variable zu einer Klasse hinzufügen, um eine gemeinsam genutzte Instanz der Klasse selbst beizubehalten, und Sie sind fertig.

Klasse Singleton static let shared = Singleton ()

Der Zugriff auf diese gemeinsam genutzte Instanz ist dann von überall in Ihrem Code einfach:

lassen Sie singleton = singleton.shared

Aus diesem Grund glauben viele Entwickler, dass sie die beste Lösung für das Problem der Staatsverbreitung gefunden haben. Sie sind aber falsch.

Das Singleton-Pattern wird eigentlich als Anti-Pattern betrachtet. In der Entwicklungsgemeinschaft wurde darüber viel diskutiert. Siehe zum Beispiel diese Stack Overflow-Frage.

Kurz gesagt: Singletons führen zu folgenden Problemen:

  • Sie führen viele Abhängigkeiten in Ihre Klassen ein und machen es schwieriger, sie in der Zukunft zu ändern.
  • Sie machen den globalen Status für jeden Teil Ihres Codes zugänglich. Dies kann zu komplexen Interaktionen führen, die schwer zu verfolgen sind und viele unerwartete Fehler verursachen.
  • Sie machen Ihre Klassen sehr schwer zu testen, da Sie sie nicht leicht von einem Einzelspieler trennen können.

An diesem Punkt denken einige Entwickler: „Ah, ich habe eine bessere Lösung. Ich werde das verwenden AppDelegate stattdessen".

Das Problem ist, dass die AppDelegate Klasse in iOS-Apps wird über die aufgerufen UIApplication gemeinsam genutzte Instanz:

Lassen Sie appDelegate = UIApplication.shared.delegate

Aber die gemeinsame Instanz von UIApplication ist selbst ein Einzelgänger. Du hast also nichts gelöst!

Die Lösung für dieses Problem ist die Abhängigkeitsinjektion. Abhängigkeitsinjektion bedeutet, dass eine Klasse keine eigenen Abhängigkeiten abruft oder erstellt, sondern diese von außen empfängt.

Um zu sehen, wie die Abhängigkeitsinjektion in iOS-Apps verwendet wird und wie das Teilen von Status ermöglicht wird, müssen wir zunächst eines der grundlegenden Architekturmuster von iOS-Apps überarbeiten: das Model-View-Controller-Muster.

Erweitern des MVC-Musters

Kurz gesagt, das MVC-Muster besagt, dass es in der Architektur einer iOS-App drei Schichten gibt:

  • Die Modellebene repräsentiert die Daten einer App.
  • Die Ansichtsebene zeigt Informationen auf dem Bildschirm und ermöglicht die Interaktion.
  • Die Controller-Schicht fungiert als Leim zwischen den beiden anderen Schichten, wobei Daten zwischen ihnen verschoben werden.

Die übliche Darstellung des MVC-Musters sieht etwa so aus:

Das Problem ist, dass dieses Diagramm falsch ist.

Dieses "Geheimnis" versteckt sich in einigen Zeilen in der Apple-Dokumentation:

„Man kann die von einem Objekt gespielten MVC-Rollen zusammenführen, wodurch ein Objekt beispielsweise sowohl die Controller- als auch die View-Rolle erfüllt. In diesem Fall würde es als View-Controller bezeichnet werden. In gleicher Weise können Sie auch Modell-Controller-Objekte haben. “

Viele Entwickler glauben, dass View Controller die einzigen Controller sind, die in einer iOS-App vorhanden sind. Aus diesem Grund wird eine Menge Code in ihnen geschrieben, weil es keinen besseren Ort gibt. Dies ist, was Entwickler dazu bringt, Singletons zu verwenden, wenn sie Status verbreiten müssen: Es scheint die einzig mögliche Lösung zu sein.

Aus den oben zitierten Zeilen ist klar, dass wir unserem Verständnis des MVC-Musters eine neue Entität hinzufügen können: den Modellcontroller. Modellcontroller befassen sich mit dem Modell der App und erfüllen die Rollen, die das Modell selbst nicht erfüllen sollte. So sollte das obige Schema aussehen:

Ein perfektes Beispiel dafür, wann ein Modell-Controller nützlich ist, ist das Beibehalten des App-Status. Das Modell sollte nur die Daten Ihrer App darstellen. Der Zustand der App sollte nicht ihr Anliegen sein.

Diese Zustandserhaltung endet normalerweise innerhalb von View-Controllern, aber jetzt haben wir einen neuen und besseren Platz, um es auszudrücken: einen Modell-Controller. Dieser Modellcontroller kann dann an View-Controller übergeben werden, wenn diese durch Abhängigkeitseingabe auf dem Bildschirm angezeigt werden.

Wir haben das Singleton-Anti-Pattern gelöst. Sehen wir unsere Lösung in der Praxis anhand eines Beispiels.

Status über View-Controller mit Hilfe der Abhängigkeitseinspritzung weiterleiten

Wir werden eine einfache App schreiben, um ein konkretes Beispiel zu sehen, wie das funktioniert. Die App zeigt Ihr Lieblingszitat auf einem Bildschirm an und ermöglicht Ihnen, das Angebot auf einem zweiten Bildschirm zu bearbeiten.

Dies bedeutet, dass unsere App zwei View-Controller benötigt, die den Status teilen müssen. Wenn Sie wissen, wie diese Lösung funktioniert, können Sie das Konzept auf Apps beliebiger Größe und Komplexität erweitern.

Um zu beginnen, benötigen wir einen Modelltyp, um die Daten darzustellen, was in unserem Fall ein Zitat ist. Dies kann mit einer einfachen Struktur erfolgen:

struct Zitieren lassen Sie Text: Zeichenfolge lassen Sie Autor: Zeichenfolge

Die Modellsteuerung

Anschließend müssen Sie einen Modellcontroller erstellen, der den Status der App enthält. Dieser Modellcontroller muss eine Klasse sein. Dies liegt daran, dass wir eine einzige Instanz benötigen, die wir an alle unsere View-Controller übergeben. Werttypen wie structs werden kopiert, wenn wir sie herumgeben, sodass sie eindeutig nicht die richtige Lösung sind.

Alle unsere Modellcontroller-Anforderungen in unserem Beispiel sind eine Eigenschaft, in der das aktuelle Angebot beibehalten werden kann. In größeren Apps können Modellcontroller natürlich komplexer sein:

class ModelController var quote = Quote (Text: "Zwei Dinge sind unendlich: das Universum und die menschliche Dummheit; und ich bin mir nicht sicher, was das Universum angeht.", Autor: "Albert Einstein")

Ich habe dem einen Standardwert zugewiesen Zitat Eigenschaft, so haben wir bereits etwas auf dem Bildschirm anzuzeigen, wenn die App gestartet wird. Dies ist nicht erforderlich, und Sie können die Eigenschaft als optional initialisieren Null, wenn Sie möchten, dass Ihre App mit einem leeren Status gestartet wird.

Erstellen Sie die Benutzeroberfläche

Wir haben jetzt den Modellcontroller, der den Status unserer App enthalten wird. Als Nächstes benötigen wir die Ansichtssteuerungen, die die Bildschirme unserer App darstellen.

Zuerst erstellen wir ihre Benutzeroberflächen. So sehen die beiden Ansichtscontroller im Storyboard der App aus.

Die Benutzeroberfläche des ersten View-Controllers besteht aus ein paar Beschriftungen und einer Schaltfläche, zusammen mit einfachen automatischen Layout-Einschränkungen. (Weitere Informationen zum automatischen Layout finden Sie hier bei Envato Tuts +.)

Die Benutzeroberfläche des zweiten View-Controllers ist dieselbe, verfügt jedoch über eine Textansicht zum Bearbeiten des Zitats und ein Textfeld zum Bearbeiten des Autors.

Die beiden Ansichtssteuerungen sind durch ein einzelnes modales Präsentationssegment verbunden, das aus dem Zitat bearbeiten Taste.

Sie können die Benutzeroberfläche und die Einschränkungen der View-Controller im GitHub-Repo untersuchen.

Codieren Sie einen View-Controller mit Abhängigkeitseinspritzung

Wir müssen jetzt unsere View-Controller codieren. Das Wichtigste, das wir dabei berücksichtigen müssen, ist, dass sie die Modellcontroller-Instanz von außen durch Abhängigkeitseinspritzung erhalten müssen. Daher müssen sie eine Eigenschaft für diesen Zweck verfügbar machen.

var modelController: ModelController!

Wir können unseren ersten View-Controller aufrufen QuoteViewController. Dieser View-Controller benötigt ein paar Auslässe für die Beschriftungen für das Angebot und den Autor in der Benutzeroberfläche.

Klasse QuoteViewController: UIViewController @IBOutlet schwach var quoteTextLabel: UILabel! @IBOutlet schwach var quoteAuthorLabel: UILabel! var modelController: ModelController! 

Wenn dieser Ansichts-Controller auf dem Bildschirm angezeigt wird, wird das aktuelle Angebot angezeigt. Wir geben den Code dazu in die Controller ein viewWillAppear (_ :) Methode.

Klasse QuoteViewController: UIViewController @IBOutlet schwach var quoteTextLabel: UILabel! @IBOutlet schwach var quoteAuthorLabel: UILabel! var modelController: ModelController! func viewWillAppear außer Kraft setzen (_ animated: Bool) super.viewWillAppear (animiert) Lassen Sie quote = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author

Wir hätten diesen Code in die viewDidLoad () stattdessen eine Methode, die durchaus üblich ist. Das Problem ist jedoch das viewDidLoad () wird nur einmal aufgerufen, wenn der View Controller erstellt wird. In unserer App müssen wir die Benutzeroberfläche von aktualisieren QuoteViewController Jedes Mal, wenn es auf dem Bildschirm erscheint. Dies liegt daran, dass der Benutzer das Angebot auf dem zweiten Bildschirm bearbeiten kann. 

Deshalb benutzen wir die viewWillAppear (_ :) Methode statt viewDidLoad (). Auf diese Weise können wir die Benutzeroberfläche des View Controllers jedes Mal aktualisieren, wenn sie auf dem Bildschirm angezeigt wird. Wenn Sie mehr über den Lebenszyklus eines View-Controllers und alle aufgerufenen Methoden erfahren möchten, habe ich einen Artikel geschrieben, in dem alle beschrieben werden.

Der Controller für die Bearbeitungsansicht

Wir müssen jetzt den zweiten View-Controller codieren. Wir werden dies nennen EditViewController.

Klasse EditViewController: UIViewController @IBOutlet schwach var textView: UITextView! @IBOutlet schwaches var textField: UITextField! var modelController: ModelController! Überschreiben Sie func viewDidLoad () super.viewDidLoad (). Lassen Sie quote = modelController.quote textView.text = quote.text textField.text = quote.author

Dieser View-Controller ist wie der vorherige:

  • Es verfügt über Auslässe für die Textansicht und das Textfeld, das der Benutzer zum Bearbeiten des Angebots verwenden wird.
  • Es hat eine Eigenschaft für die Abhängigkeitsinjektion der Modellcontrollerinstanz.
  • Es füllt seine Benutzeroberfläche, bevor es auf dem Bildschirm erscheint.

In diesem Fall habe ich die verwendet viewDidLoad () Diese Ansichtssteuerung wird nur einmal angezeigt.

Den Staat teilen

Nun müssen wir den Status zwischen den beiden View-Controllern übergeben und aktualisieren, wenn der Benutzer das Angebot bearbeitet.

Wir übergeben den App-Status im vorbereiten (für: Absender :) Methode von QuoteViewController. Diese Methode wird durch das verbundene Segment ausgelöst, wenn der Benutzer auf das Symbol tippt Zitat bearbeiten Taste.

Klasse QuoteViewController: UIViewController @IBOutlet schwach var quoteTextLabel: UILabel! @IBOutlet schwach var quoteAuthorLabel: UILabel! var modelController: ModelController! func viewWillAppear überschreiben (_ animated: Bool) super.viewWillAppear (animiert) Lassen Sie quote = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author override func sich vorbereiten (für Auswahl: UIStoryboardSegue, Absender: beliebig? ) wenn wir editViewController = segue.destination as lassen? EditViewController editViewController.modelController = modelController

Hier übergeben wir die Instanz von ModelController Das hält den Status der App. Hier setzt die Abhängigkeitsinjektion für die EditViewController das passiert.

In dem EditViewController, Wir müssen den Status auf das neu eingegebene Angebot aktualisieren, bevor wir zum vorherigen View-Controller zurückkehren. Wir können dies in einer Aktion tun, die mit der sparen Taste:

Klasse EditViewController: UIViewController @IBOutlet schwach var textView: UITextView! @IBOutlet schwaches var textField: UITextField! var modelController: ModelController! Überschreiben Sie func viewDidLoad () super.viewDidLoad () lassen Sie quote = modelController.quote textView.text = quote.text textField.text = quote.author @IBAction func save (_ sender: AnyObject) lassen Sie newQuote = Quote ( textView.text, Autor: textField.text!) modelController.quote = newQuote verwerfen (animiert: true, Completion: nil)

Initialisieren Sie den Model Controller

Wir sind fast fertig, aber Sie haben vielleicht bemerkt, dass uns noch etwas fehlt: der QuoteViewController übergibt das ModelController zum EditViewController durch Abhängigkeitsinjektion. Aber wer gibt dieser Instanz den QuoteViewController an erster Stelle? Denken Sie daran, dass ein View Controller bei der Verwendung der Abhängigkeitseinspritzung keine eigenen Abhängigkeiten erstellen darf. Diese müssen von außen kommen.

Es gibt aber keinen View Controller vor dem QuoteViewController, denn dies ist der erste View Controller unserer App. Wir brauchen ein anderes Objekt, um das zu erstellen ModelController Instanz und an die QuoteViewController.

Dieses Objekt ist das AppDelegate. Die Rolle des App-Delegierten besteht darin, auf die Lebenszyklusmethoden der App zu reagieren und die App entsprechend zu konfigurieren. Eine dieser Methoden ist Anwendung (_: didFinishLaunchingWithOptions :), Die wird aufgerufen, sobald die App gestartet wird. Dort erstellen wir die Instanz von ModelController und gib es dem QuoteViewController:

Klasse AppDelegate: UIResponder, UIApplicationDelegate Var-Fenster: UIWindow? func-Anwendung (_ Anwendung: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool wenn QuoteViewController = Window? .rootViewController as? QuoteViewController quoteViewController.modelController = ModelController () gibt true zurück

Unsere App ist jetzt fertig. Jeder View-Controller erhält Zugriff auf den globalen Status der App. In unserem Code werden jedoch keine Singletons verwendet.

Sie können das Xcode-Projekt für diese Beispiel-App im Tutorial GitHub-Repo herunterladen.

Schlussfolgerungen

In diesem Artikel haben Sie gesehen, dass die Verwendung von Singletons zur Verbreitung des Status in einer iOS-App eine schlechte Praxis ist. Singletons verursachen viele Probleme, obwohl sie sehr einfach zu erstellen und zu verwenden sind.

Wir haben das Problem gelöst, indem wir uns das MVC-Muster genauer angesehen und die darin verborgenen Möglichkeiten verstanden haben. Durch die Verwendung von Modellcontrollern und Abhängigkeitseinspritzung konnten wir den Status der App über alle Ansichtscontroller ausbreiten, ohne Singletons zu verwenden.

Dies ist eine einfache Beispielanwendung, das Konzept kann jedoch auf Anwendungen beliebiger Komplexität verallgemeinert werden. Dies ist die bewährte Standardmethode, um den Status in iOS-Apps zu verbreiten. Ich verwende es jetzt in jeder App, die ich für meine Kunden schreibe.

Ein paar Dinge, die Sie beachten sollten, wenn Sie das Konzept auf größere Apps erweitern:

  • Der Modellcontroller kann den Status der App beispielsweise in einer Datei speichern. Auf diese Weise werden unsere Daten bei jedem Schließen der App gespeichert. Sie können auch eine komplexere Speicherlösung verwenden, z. B. Core Data. Meine Empfehlung ist, diese Funktionalität in einem separaten Modellcontroller zu speichern, der nur für die Speicherung sorgt. Dieser Controller kann dann von dem Modellcontroller verwendet werden, der den Status der App beibehält.
  • In einer App mit einem komplexeren Fluss haben Sie viele Container in Ihrem App-Fluss. Dies sind in der Regel Navigationssteuerungen mit gelegentlichen Registerkartenleistensteuerungen. Das Konzept der Abhängigkeitsinjektion gilt weiterhin, aber Sie müssen die Container berücksichtigen. Sie können entweder in die enthaltenen View-Controller einsteigen, wenn Sie die Abhängigkeitseinspritzung durchführen, oder benutzerdefinierte Container-Unterklassen erstellen, die den Modell-Controller übergeben.
  • Wenn Sie Ihrer App ein Netzwerk hinzufügen, sollte dies auch in einem separaten Modellcontroller erfolgen. Ein Ansichtscontroller kann eine Netzwerkanforderung über diesen Netzwerkcontroller ausführen und die resultierenden Daten dann an den Modellcontroller weiterleiten, der den Status beibehält. Denken Sie daran, dass die Aufgabe eines View-Controllers genau diese Aufgabe ist: Als Glue-Objekt zu fungieren, das Daten zwischen Objekten weitergibt.

Bleiben Sie dran, um weitere Tipps und Best Practices für die Entwicklung von iOS-Apps zu erhalten!