
Derzeit sind VIPER und MVVM die beliebtesten Architekturlösungen für die Entwicklung großer Anwendungen, die die Teilnahme an der Entwicklung großer Teams erfordern, die gut getestet, langfristig unterstützt und ständig weiterentwickelt werden. In diesem Artikel werden wir versuchen, sie auf ein kleines Testprojekt anzuwenden. Hierbei handelt es sich um eine Liste von Benutzerkontakten mit der Möglichkeit, einen neuen Kontakt hinzuzufügen. Dieser Artikel hat mehr Übung als Analytik und richtet sich in erster Linie an diejenigen, die theoretisch bereits mit diesen Architekturen vertraut sind und nun anhand spezifischer Beispiele verstehen möchten, wie dies funktioniert. Es gibt jedoch auch eine grundlegende Beschreibung der Architekturen und ihres Vergleichs.
Dieser Artikel ist eine Übersetzung von Rafael Sacchis Artikel
„Vergleichen von MVVM- und Viper-Architekturen: Verwendung der einen oder anderen Architektur“ . Leider wurde irgendwann bei der Erstellung des Artikels "Veröffentlichung" anstelle von "Übersetzung" eingerichtet, sodass Sie hier schreiben müssen.
Eine gut gestaltete Architektur ist unerlässlich, um die kontinuierliche Unterstützung Ihres Projekts sicherzustellen. In diesem Artikel werden MVVM- und VIPER-Architekturen als Alternative zu herkömmlichen MVC betrachtet.
MVC ist ein bekanntes Konzept für alle, die seit geraumer Zeit in der Softwareentwicklung tätig sind. Dieses Muster unterteilt das Projekt in drei Teile: Modell, das Entitäten darstellt; Ansicht, eine Schnittstelle für die Benutzerinteraktion; und Controller, der für die Sicherstellung der Interaktion zwischen Ansicht und Modell verantwortlich ist. Dies ist die Architektur, die Apple uns zur Verwendung in unseren Anwendungen anbietet.
Sie wissen jedoch wahrscheinlich, dass Projekte eine Menge komplexer Funktionen bieten: Unterstützung für Netzwerkanforderungen, Parsen, Zugriff auf Datenmodelle, Konvertieren von Daten für die Ausgabe, Reaktion auf Schnittstellenereignisse usw. Als Ergebnis erhalten Sie riesige Controller, die die oben genannten Aufgaben lösen, und eine Menge Code, der nicht wiederverwendet werden kann. Mit anderen Worten, MVC kann ein Albtraum für einen Entwickler mit langfristiger Projektunterstützung sein. Aber wie kann eine hohe Modularität und Wiederverwendbarkeit in iOS-Projekten sichergestellt werden?
Wir werden uns zwei sehr berühmte Alternativen zur MVC-Architektur ansehen: MVVM und VIPER. Beide sind in der iOS-Community sehr bekannt und haben bewiesen, dass sie eine großartige Alternative zu MVC sein können. Wir werden über ihre Struktur sprechen, eine Beispielanwendung schreiben und Fälle betrachten, in denen es besser ist, die eine oder andere Architektur zu verwenden.
BeispielWir werden eine Anwendung mit einer Tabelle von Benutzerkontakten schreiben. Sie können den Code aus
diesem Repository verwenden . In den Starter-Ordnern ist das Grundgerüst des Projekts enthalten, und in den endgültigen Ordnern befindet sich eine vollständig abgeschlossene Anwendung.
Die Anwendung verfügt über zwei Bildschirme: Auf dem ersten Bildschirm wird eine Liste der Kontakte in Form einer Tabelle angezeigt. Die Zelle enthält den Vor- und Nachnamen des Kontakts sowie das Basisbild anstelle des Benutzerbilds.

Der zweite Bildschirm ist der Bildschirm zum Hinzufügen eines neuen Kontakts mit den Eingabefeldern für Vor- und Nachnamen sowie den Schaltflächen Fertig und Abbrechen.
MVVMWie es funktioniert:
MVVM steht für
Model-View-ViewModel . Dieser Ansatz unterscheidet sich von MVC in der Logik der Verantwortlichkeitsverteilung zwischen Modulen.
- Modell : Dieses Modul unterscheidet sich nicht von dem in MVC. Er ist für die Erstellung von Datenmodellen verantwortlich und kann Geschäftslogik enthalten. Sie können beispielsweise auch Hilfsklassen erstellen, z. B. eine Manager-Klasse zum Verwalten von Objekten in Model und einen Netzwerk-Manager zum Verarbeiten von Netzwerkanforderungen und zum Parsen.
- Ansicht : Und hier beginnt sich alles zu ändern. Das View-Modul in MVVM umfasst die Benutzeroberfläche (Unterklassen von UIView-, .xib- und .storyboard-Dateien), die Anzeigelogik (Animation, Rendering) und die Behandlung von Benutzerereignissen (Tastendruck usw.). View und Controller sind in MVC dafür verantwortlich. Dies bedeutet, dass Ihre Ansichten unverändert bleiben, während der ViewController einen kleinen Teil dessen enthält, was in MVC enthalten war, und dementsprechend stark abnimmt.
- ViewModel : Dies ist jetzt der Ort, an dem sich der größte Teil des Codes befindet, den Sie zuvor in ViewController hatten. Die ViewModel-Schicht fordert Daten vom Modell an (dies kann eine Anforderung an eine lokale Datenbank oder eine Netzwerkanforderung sein) und überträgt sie zurück an die Ansicht, bereits in dem Format, in dem sie dort verwendet und angezeigt werden. Dies ist jedoch ein bidirektionaler Mechanismus. Aktionen oder Daten, die vom Benutzer eingegeben werden, durchlaufen das ViewModel und aktualisieren das Modell. Da das ViewModel alles anzeigt, was angezeigt wird, ist es nützlich, den Verknüpfungsmechanismus zwischen den beiden Ebenen zu verwenden.
Im Vergleich zu MVC wechseln Sie von einer Architektur, die folgendermaßen aussieht:

Zur nächsten Architekturvariante:

In denen die Klassen und Unterklassen von UIView und UIViewController zum Implementieren der Ansicht verwendet werden.
Nun zum Punkt. Lassen Sie uns ein Beispiel unserer Anwendung unter Verwendung der MVVM-Architektur schreiben.
MVVM Kontakte AppMODELLDie folgende Klasse ist ein
Kontaktkontaktmodell :
import CoreData open class Contact: NSManagedObject { @NSManaged var firstName: String? @NSManaged var lastName: String? var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " \(lastName)" } return name } } }
Die Kontaktklasse enthält die Felder
firstName ,
lastName sowie die berechnete Eigenschaft
fullName .
ANSICHTVIEW enthält: Haupt-Storyboard mit bereits platzierten Ansichten; ContactsViewController, der eine Liste der Kontakte in einer Tabelle anzeigt; und AddContactViewController mit zwei Beschriftungen und Eingabefeldern, um den Vor- und Nachnamen des neuen Kontakts hinzuzufügen. Beginnen wir mit dem
ContactsViewController . Der Code sieht folgendermaßen aus:
import UIKit class ContactsViewController: UIViewController { @IBOutlet var tableView: UITableView! let contactViewModelController = ContactViewModelController() override func viewDidLoad() { super.viewDidLoad() tableView.tableFooterView = UIView() contactViewModelController.retrieveContacts({ [unowned self] in self.tableView.reloadData() }, failure: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let addContactNavigationController = segue.destination as? UINavigationController let addContactVC = addContactNavigationController?.viewControllers[0] as? AddContactViewController addContactVC?.contactsViewModelController = contactViewModelController addContactVC?.didAddContact = { [unowned self] (contactViewModel, index) in let indexPath = IndexPath(row: index, section: 0) self.tableView.beginUpdates() self.tableView.insertRows(at: [indexPath], with: .left) self.tableView.endUpdates() } } } extension ContactsViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") as? ContactsTableViewCell guard let contactsCell = cell else { return UITableViewCell() } contactsCell.cellModel = contactViewModelController.viewModel(at: (indexPath as NSIndexPath).row) return contactsCell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactViewModelController.contactsCount } }
Selbst mit einem flüchtigen Blick ist klar, dass diese Klasse zum größten Teil Schnittstellenaufgaben implementiert. Es hat auch eine Navigation in der
prepareForSegue (: :) -Methode - und genau dies ist der Moment, der sich in VIPER ändert, wenn eine Router-Ebene hinzugefügt wird.
Schauen wir uns die Klassenerweiterung genauer an, die das UITableViewDataSource-Protokoll implementiert. Funktionen funktionieren nicht direkt mit dem Kontaktmodell des Kontaktbenutzers in der Modellebene. Stattdessen erhalten sie Daten (dargestellt durch die ContactViewModel-Struktur) in der Form, in der sie angezeigt werden, die bereits mit ViewModelController formatiert wurden.
Das gleiche passiert in einer Schaltung, die unmittelbar nach dem Erstellen eines Kontakts beginnt. Seine einzige Aufgabe ist es, der Tabelle eine Zeile hinzuzufügen und die Schnittstelle zu aktualisieren.
Jetzt müssen Sie eine Beziehung zwischen der Unterklasse von UITableViewCell und ViewModel herstellen. Dies würde wie die Zellenklasse der
ContactsTableViewCell- Tabelle aussehen:
import UIKit class ContactsTableViewCell: UITableViewCell { var cellModel: ContactViewModel? { didSet { bindViewModel() } } func bindViewModel() { textLabel?.text = cellModel?.fullName } }
Und so ist die
AddContactViewController- Klasse:
import UIKit class AddContactViewController: UIViewController { @IBOutlet var firstNameTextField: UITextField! @IBOutlet var lastNameTextField: UITextField! var contactsViewModelController: ContactViewModelController? var didAddContact: ((ContactViewModel, Int) -> Void)? override func viewDidLoad() { super.viewDidLoad() firstNameTextField.becomeFirstResponder() } @IBAction func didClickOnDoneButton(_ sender: UIBarButtonItem) { guard let firstName = firstNameTextField.text, let lastName = lastNameTextField.text else { return } if firstName.isEmpty || lastName.isEmpty { showEmptyNameAlert() return } dismiss(animated: true) { [unowned self] in self.contactsViewModelController?.createContact(firstName: firstName, lastName: lastName, success: self.didAddContact, failure: nil) } } @IBAction func didClickOnCancelButton(_ sender: UIBarButtonItem) { dismiss(animated: true, completion: nil) } fileprivate func showEmptyNameAlert() { showMessage(title: "Error", message: "A contact must have first and last names") } fileprivate func showMessage(title: String, message: String) { let alertView = UIAlertController(title: title, message: message, preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "Ok", style: .destructive, handler: nil)) present(alertView, animated: true, completion: nil) } }
Auch hier wird hauptsächlich mit der Benutzeroberfläche gearbeitet. Beachten Sie, dass AddContactViewController die Funktionen zur Kontakterstellung in der Funktion
didClickOnDoneButton (:) an ViewModelController
delegiert .
MODELL ANZEIGENEs ist Zeit, über die völlig neue ViewModel-Ebene für uns zu sprechen. Erstellen Sie zunächst eine
ContactViewModel- Kontaktklasse, die die anzuzeigende Ansicht bereitstellt, und <und> Funktionen mit Parametern werden zum Sortieren von Kontakten definiert:
public struct ContactViewModel { var fullName: String } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() }
Der ContactViewModelController- Code sieht folgendermaßen aus:
class ContactViewModelController { fileprivate var contactViewModelList: [ContactViewModel] = [] fileprivate var dataManager = ContactLocalDataManager() var contactsCount: Int { return contactViewModelList.count } func retrieveContacts(_ success: (() -> Void)?, failure: (() -> Void)?) { do { let contacts = try dataManager.retrieveContactList() contactViewModelList = contacts.map() { ContactViewModel(fullName: $0.fullName) } success?() } catch { failure?() } } func viewModel(at index: Int) -> ContactViewModel { return contactViewModelList[index] } func createContact(firstName: String, lastName: String, success: ((ContactViewModel, Int) -> Void)?, failure: (() -> Void)?) { do { let contact = try dataManager.createContact(firstName: firstName, lastName: lastName) let contactViewModel = ContactViewModel(fullName: contact.fullName) let insertionIndex = contactViewModelList.insertionIndex(of: contactViewModel) { $0 < $1 } contactViewModelList.insert(contactViewModel, at: insertionIndex) success?(contactViewModel, insertionIndex) } catch { failure?() } } }
Hinweis: MVVM gibt keine genaue Definition zum Erstellen eines ViewModel. Wenn ich eine mehrschichtige Architektur erstellen möchte, bevorzuge ich die Erstellung eines ViewModelControllers, der mit der Modellebene interagiert und für die Erstellung der ViewModel-Objekte verantwortlich ist.
Die Hauptsache, die sehr leicht zu merken ist: Die ViewModel-Ebene sollte nicht an der Arbeit mit der Benutzeroberfläche beteiligt sein. Um dies zu vermeiden, ist es besser, UIKit
niemals mit ViewModel in eine Datei zu importieren.
Die ContactViewModelController-Klasse fordert Kontakte vom lokalen Speicher an und versucht, die Modellebene nicht zu beeinflussen. Es gibt die Daten in dem Format zurück, in dem die Ansicht angezeigt werden muss, und benachrichtigt die Ansicht, wenn ein neuer Kontakt hinzugefügt wird und sich die Daten ändern.
Im wirklichen Leben wäre dies eine Netzwerkanforderung und keine Anforderung an die lokale Datenbank, aber in keinem Fall sollte eine von ihnen Teil des ViewModel sein - sowohl die Netzwerkarbeit als auch die Arbeit mit der lokalen Datenbank sollten mit ihren eigenen Managern bereitgestellt werden ( Manager).
Das ist alles über MVVM. Vielleicht erscheint Ihnen dieser Ansatz testbarer, unterstützter und verteilter als MVC. Lassen Sie uns nun über VIPER sprechen und sehen, wie es sich von MVVM unterscheidet.
VIPERWie es funktioniert:
VIPER ist eine Clean Architecture-Implementierung für iOS-Projekte. Die Struktur besteht aus: Ansicht, Interaktor, Präsentator, Entität und Router. Dies ist wirklich eine sehr verteilte und modulare Architektur, die es Ihnen ermöglicht, Verantwortung zu teilen, die durch Unit-Tests sehr gut abgedeckt ist und Ihren Code wiederverwendbar macht.
- Ansicht : Eine Schnittstellenebene, die normalerweise UIKit-Dateien (einschließlich UIViewController) impliziert. Es ist verständlich, dass in verteilten Systemen Unterklassen des UIViewController mit View verknüpft sein sollten. In VIPER sind die Dinge fast die gleichen wie in MVVM: View ist dafür verantwortlich, anzuzeigen, was Presenter gibt, und vom Benutzer eingegebene Informationen oder Aktionen an Presenter zu übertragen.
- Interactor : Enthält die Geschäftslogik, die für das Funktionieren der Anwendung erforderlich ist. Interactor ist für das Abrufen von Daten aus dem Modell (Netzwerk- oder lokale Anforderungen) verantwortlich, und seine Implementierung hängt in keiner Weise mit der Benutzeroberfläche zusammen. Es ist wichtig zu beachten, dass Netzwerk- und lokale Manager nicht Teil von VIPER sind, sondern als separate Abhängigkeiten behandelt werden.
- Moderator : Verantwortlich für die Formatierung der Daten, die in der Ansicht angezeigt werden sollen. In MVVM in unserem Beispiel war ViewModelController dafür verantwortlich. Presenter empfängt Daten von Interactor, erstellt eine Instanz von ViewModel (eine formatierte Klasse für die korrekte Anzeige) und übergibt sie an View. Er reagiert auch auf Benutzereingaben von Daten, fordert zusätzliche Daten aus der Datenbank an oder gibt sie an sie weiter.
- Entität : Übernimmt die Verantwortung für die Modellebene, die in anderen Architekturen verwendet wird. Entität ist ein einfaches Datenobjekt ohne Geschäftslogik, das von einem Online-Traktor und verschiedenen Datenmanagern verwaltet wird.
- Router : Alle Anwendungsnavigationslogik. Es scheint, dass dies nicht die wichtigste Ebene ist. Wenn Sie jedoch beispielsweise dieselbe Ansicht sowohl auf dem iPhone als auch in der Anwendung für das iPad wiederverwenden müssen, kann sich nur ändern, wie Ihre Ansichten auf dem Bildschirm angezeigt werden. Auf diese Weise können Sie keine weiteren Ebenen außer dem Router berühren, der jeweils dafür verantwortlich ist.
Im Vergleich zu MVVM weist VIPER mehrere wesentliche Unterschiede in der Verteilung der Verantwortung auf:
- Er hat einen Router, eine separate Ebene, die für die Navigation verantwortlich ist
- Entitäten sind einfache Datenobjekte, die die Verantwortung für den Zugriff auf Daten vom Modell zum Interactor neu verteilen
- Die Verantwortlichkeiten von ViewModelController werden zwischen Interactor und Presenter aufgeteilt
Und jetzt wiederholen wir die gleiche Anwendung, aber bereits auf VIPER. Zum besseren Verständnis erstellen wir jedoch nur einen Controller mit Kontakten. Den Code für den Controller zum Hinzufügen eines neuen Kontakts im Projekt finden Sie über den Link (VIPER Contacts Starter-Ordner in
diesem Repository ).
Hinweis : Wenn Sie Ihr Projekt auf VIPER erstellen möchten, sollten Sie nicht versuchen, alle Dateien manuell zu erstellen. Sie können beispielsweise einen der Codegeneratoren verwenden, z. B.
VIPER Gen oder
Generamba (Rambler-Projekt) .
VIPER Kontakte AppANSICHTVIEW wird durch Elemente aus Main.storyboard und der ContactListView-Klasse dargestellt. VIEW ist sehr passiv; Seine einzige Aufgabe besteht darin, Schnittstellenereignisse an Presenter zu übertragen und seinen Status nach Benachrichtigung durch Presenter zu aktualisieren. So sieht der
ContactListView- Code aus:
import UIKit class ContactListView: UIViewController { @IBOutlet var tableView: UITableView! var presenter: ContactListPresenterProtocol? var contactList: [ContactViewModel] = [] override func viewDidLoad() { super.viewDidLoad() presenter?.viewDidLoad() tableView.tableFooterView = UIView() } @IBAction func didClickOnAddButton(_ sender: UIBarButtonItem) { presenter?.addNewContact(from: self) } } extension ContactListView: ContactListViewProtocol { func reloadInterface(with contacts: [ContactViewModel]) { contactList = contacts tableView.reloadData() } func didInsertContact(_ contact: ContactViewModel) { let insertionIndex = contactList.insertionIndex(of: contact) { $0 < $1 } contactList.insert(contact, at: insertionIndex) let indexPath = IndexPath(row: insertionIndex, section: 0) tableView.beginUpdates() tableView.insertRows(at: [indexPath], with: .right) tableView.endUpdates() } } extension ContactListView: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") else { return UITableViewCell() } cell.textLabel?.text = contactList[(indexPath as NSIndexPath).row].fullName return cell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactList.count } }
View sendet die Ereignisse
viewDidLoad und
didClickOnAddButton an den Presenter. Beim ersten Ereignis fordert der Präsentator Daten von Interactor an, und beim zweiten Ereignis fordert der Präsentator den Router auf, zum Hinzufügen eines neuen Kontakts zum Controller zu wechseln.
ContactListViewProtocol-Protokollmethoden werden von Presenter entweder aufgerufen, wenn eine Kontaktliste angefordert wird oder wenn ein neuer Kontakt hinzugefügt wird. In beiden Fällen enthalten die Daten in der Ansicht nur die Informationen, die für die Anzeige erforderlich sind.
In der Ansicht befinden sich auch Methoden, die das UITableViewDataSource-Protokoll implementieren und die Tabelle mit den empfangenen Daten füllen.
INTERAKTORInteractor in unserem Beispiel ist ganz einfach. Er fordert lediglich Daten über den lokalen Datenbankmanager an, und es spielt für ihn keine Rolle, was dieser Manager, CoreData, Realm oder eine andere Lösung verwendet. Der Code in ContactListInteractor lautet wie folgt:
class ContactListInteractor: ContactListInteractorInputProtocol { weak var presenter: ContactListInteractorOutputProtocol? var localDatamanager: ContactListLocalDataManagerInputProtocol? func retrieveContacts() { do { if let contactList = try localDatamanager?.retrieveContactList() { presenter?.didRetrieveContacts(contactList) } else { presenter?.didRetrieveContacts([]) } } catch { presenter?.didRetrieveContacts([]) } } }
Nachdem Interactor die angeforderten Daten empfangen hat, benachrichtigt es Presenter. Optional kann Interactor einen Fehler an Presenter senden, der den Fehler dann in eine Ansicht formatieren muss, die für die Anzeige in der Ansicht geeignet ist.
Hinweis : Wie Sie vielleicht bemerkt haben, implementiert jede Schicht in VIPER ein Protokoll. Infolgedessen hängen Klassen von Abstraktionen und nicht von einer bestimmten Implementierung ab und erfüllen somit das Prinzip der Abhängigkeitsinversion (eines der Prinzipien von SOLID).
PRESENTERDas wichtigste Element der Architektur. Die gesamte Kommunikation zwischen der Ansicht und den übrigen Ebenen (Interactor und Router) erfolgt über den Presenter.
ContactListPresenter Code:
class ContactListPresenter: ContactListPresenterProtocol { weak var view: ContactListViewProtocol? var interactor: ContactListInteractorInputProtocol? var wireFrame: ContactListWireFrameProtocol? func viewDidLoad() { interactor?.retrieveContacts() } func addNewContact(from view: ContactListViewProtocol) { wireFrame?.presentAddContactScreen(from: view) } } extension ContactListPresenter: ContactListInteractorOutputProtocol { func didRetrieveContacts(_ contacts: [Contact]) { view?.reloadInterface(with: contacts.map() { return ContactViewModel(fullName: $0.fullName) }) } } extension ContactListPresenter: AddModuleDelegate { func didAddContact(_ contact: Contact) { let contactViewModel = ContactViewModel(fullName: contact.fullName) view?.didInsertContact(contactViewModel) } func didCancelAddContact() {} }
Nach dem Laden von View wird Presenter benachrichtigt, der wiederum Daten über Interactor anfordert. Wenn der Benutzer auf die Schaltfläche Neuen Kontakt hinzufügen klickt, benachrichtigt View den Präsentator, der eine Anforderung zum Öffnen des Bildschirms Neuen Kontakt hinzufügen im Router sendet.
Presenter formatiert die Daten auch und gibt sie nach Abfrage der Kontaktliste an die Ansicht zurück. Er ist auch für die Implementierung des AddModuleDelegate-Protokolls verantwortlich. Dies bedeutet, dass Presenter eine Benachrichtigung erhält, wenn ein neuer Kontakt hinzugefügt wird. Bereiten Sie die Kontaktdaten für die Anzeige vor und übertragen Sie sie an View.
Wie Sie vielleicht bemerkt haben, hat Presenter jede Chance, ziemlich umständlich zu werden. Wenn es eine solche Möglichkeit gibt, kann Presenter in zwei Teile unterteilt werden: Presenter, der nur Daten empfängt, formatiert sie zur Anzeige und übergibt sie an View; und einen Ereignishandler, der auf Benutzeraktionen reagiert.
ENTITÄTDiese Ebene ähnelt der Modellebene in MVVM. In unserer Anwendung wird es durch die Kontaktklassen- und Operatordefinitionsfunktionen <und> dargestellt.
Der Kontaktinhalt sieht folgendermaßen aus:
import CoreData open class Contact: NSManagedObject { var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " " + lastName } return name } } } public struct ContactViewModel { var fullName = "" } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() }
ContactViewModel enthält die Felder, die Presenter ausfüllt (Formate), die in der Ansicht angezeigt werden. Die Contact-Klasse ist eine Unterklasse von NSManagedObject, die dieselben Felder wie im CoreData-Modell enthält.
ROUTERUnd schließlich die letzte, aber sicherlich nicht wichtige Schicht. Alle Verantwortung für die Navigation liegt bei Presenter und WireFrame. Presenter empfängt ein Ereignis vom Benutzer und weiß, wann der Übergang durchgeführt werden muss, und WireFrame weiß, wie und wo dieser Übergang durchgeführt werden soll. Damit Sie nicht verwirrt werden, wird in diesem Beispiel die Router-Ebene durch die ContactListWireFrame-Klasse dargestellt und im Text als WireFrame bezeichnet.
ContactListWireFrame Code:
import UIKit class ContactListWireFrame: ContactListWireFrameProtocol { class func createContactListModule() -> UIViewController { let navController = mainStoryboard.instantiateViewController(withIdentifier: "ContactsNavigationController") if let view = navController.childViewControllers.first as? ContactListView { let presenter: ContactListPresenterProtocol & ContactListInteractorOutputProtocol = ContactListPresenter() let interactor: ContactListInteractorInputProtocol = ContactListInteractor() let localDataManager: ContactListLocalDataManagerInputProtocol = ContactListLocalDataManager() let wireFrame: ContactListWireFrameProtocol = ContactListWireFrame() view.presenter = presenter presenter.view = view presenter.wireFrame = wireFrame presenter.interactor = interactor interactor.presenter = presenter interactor.localDatamanager = localDataManager return navController } return UIViewController() } static var mainStoryboard: UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) } func presentAddContactScreen(from view: ContactListViewProtocol) { guard let delegate = view.presenter as? AddModuleDelegate else { return } let addContactsView = AddContactWireFrame.createAddContactModule(with: delegate) if let sourceView = view as? UIViewController { sourceView.present(addContactsView, animated: true, completion: nil) } } }
Da WireFrame für die Erstellung des Moduls verantwortlich ist, können Sie hier alle Abhängigkeiten konfigurieren. Wenn Sie einen anderen Controller öffnen möchten, empfängt die Funktion, die den neuen Controller öffnet, als Argument das Objekt, das ihn öffnet, und erstellt mithilfe seines WireFrame einen neuen Controller. Außerdem werden beim Erstellen eines neuen Controllers die erforderlichen Daten an diesen übertragen, in diesem Fall nur der Delegat (Präsentator des Controllers mit Kontakten), um den erstellten Kontakt zu empfangen.
Die Router-Ebene bietet eine gute Möglichkeit, die Verwendung von Übergängen (Übergängen) in Storyboards zu vermeiden und die gesamte Code-Navigation zu organisieren. Da Storyboards keine kompakte Lösung für die Datenübertragung zwischen Controllern bieten, wird in unserer Navigationsimplementierung kein zusätzlicher Code hinzugefügt. Wir erhalten nur die beste Wiederverwendbarkeit.
Zusammenfassung :
Sie finden beide Projekte in
diesem Repository .
Wie Sie sehen können, sind MVVM und VIPER zwar unterschiedlich, aber nicht eindeutig. MVVM sagt uns, dass es neben Ansicht und Modell auch eine ViewModel-Ebene geben sollte. Es wird jedoch nichts darüber gesagt, wie diese Ebene erstellt werden soll oder wie die Daten angefordert werden - die Verantwortung für diese Ebene ist nicht klar definiert. Es gibt viele Möglichkeiten, es zu implementieren, und Sie können jede davon verwenden.
VIPER hingegen ist eine ziemlich einzigartige Architektur. Es besteht aus vielen Schichten, von denen jede einen genau definierten Verantwortungsbereich hat und weniger als MVVM vom Entwickler beeinflusst wird.
Wenn es um die Auswahl einer Architektur geht, gibt es normalerweise nicht die einzig richtige Lösung, aber ich werde trotzdem versuchen, ein paar Tipps zu geben. Wenn Sie ein großes und langwieriges Projekt mit klaren Anforderungen haben und ausreichend Gelegenheit zur Wiederverwendung von Komponenten haben möchten, ist VIPER die beste Lösung. Eine klarere Abgrenzung der Verantwortung ermöglicht es, Tests besser zu organisieren und die Wiederverwendung zu verbessern.