Blog

Vereinfachung von iOS View Controllern: MVVM oder Präsentationssteuerungen?

Lammert Westerhoff

Lammert Westerhoff

Aktualisiert Oktober 22, 2025
11 Minuten

Wir alle haben schon View Controller gesehen, die aus Hunderten oder sogar Tausenden von Codezeilen bestehen. Eine beliebte Strategie zur Reduzierung der Komplexität von View Controllern ist die Verwendung des Model-View-ViewModel (MVVM)-Designmusters. Aber das ist nicht die einzige Möglichkeit, die View-Controller in kleinere, leichter zu verstehende Komponenten aufzuteilen. In diesem Beitrag werden wir uns mit der Verwendung von kleinen Presentation Control-Klassen beschäftigen, um einen ähnlichen Effekt zu erzielen. Sie können sogar zusammen mit MVVM-Komponenten verwendet werden.

In diesem Beitrag wird das MVVM-Muster nicht im Detail behandelt. Wenn Sie mehr darüber lesen möchten, folgen Sie bitte einem der Links am Ende dieses Beitrags. Sie sollten jedoch in der Lage sein, den größten Teil dieses Beitrags auch ohne Vorkenntnisse über MVVM zu verstehen. Zunächst zeigen wir ein traditionelles Beispiel für einen View Controller, gefolgt von einem Beispiel mit MVVM und einem weiteren Beispiel mit Presentation Controls. Am Ende werden wir beide miteinander vergleichen und sehen, wie wir sie zusammen verwenden können.

Problem: Komplexer View Controller

Das Problem, das wir hier zu lösen versuchen, ist, dass View-Controller oft zu viele Funktionen enthalten. Neben der Aktualisierung der Ansicht und der Bearbeitung von Benutzerinteraktionen sind sie oft voll mit Status, Netzwerkanfragen, Fehlerbehandlung, Validierung und so weiter. Schauen wir uns ein Beispiel für einen traditionellen View Controller an. Stellen Sie sich vor, wir haben eine Reiseplanungs-App, in der wir die Abfahrts- und Ankunftszeit einer Reise anzeigen, die wir demnächst unternehmen wollen. Dies könnte in etwa so aussehen:

Blog

All diese Informationen stammen aus einem ziemlich einfachen Modell: [code language="obj-c"] class Trip { let departure: NSDate let arrival: NSDate let actualDeparture: NSDate init(departure: NSDate, arrival: NSDate, actualDeparture: NSDate? = nil) { self.departure = departure self.arrival = arrival self.actualDeparture = actualDeparture ?? departure } } [/code] Das Modell hat eine Abfahrtszeit, eine Ankunftszeit und eine tatsächliche Abfahrtszeit. Die Ansicht zeigt oben rechts das Datum (wir gehen der Einfachheit halber davon aus, dass Abfahrts- und Ankunftsdatum immer gleich sind), links die Abfahrtszeit und rechts die Ankunftszeit. Zwischen den Abfahrts- und Ankunftszeiten sehen Sie die Dauer. Wenn die tatsächliche Abfahrtszeit später ist als die Abfahrtszeit, bedeutet dies, dass es eine Verspätung gibt. In diesem Fall wird die Abfahrtszeit in rot und die Verspätung unten angezeigt. Ohne MVVM oder Presentation Controls würde der Code in unserem View Controller wie folgt aussehen: [code language="obj-c"] class ViewController: UIViewController { @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var departureTimeLabel: UILabel! @IBOutlet weak var arrivalTimeLabel: UILabel! @IBOutlet weak var durationLabel: UILabel! @IBOutlet weak var delayLabel: UILabel! // In einer echten App würde die Reise von einem anderen View Controller aus festgelegt oder von einem Server geladen werden... let trip = Trip(Abfahrt: NSDate(timeIntervalSince1970: 1444396193), arrival: NSDate(timeIntervalSince1970: 1444397193), actualDeparture: NSDate(timeIntervalSince1970: 1444396493)) override func viewDidLoad() { super.viewDidLoad() dateLabel.text = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .ShortStyle, timeStyle: .NoStyle) departureTimeLabel.text = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .NoStyle, timeStyle: .ShortStyle) arrivalTimeLabel.text = NSDateFormatter.localizedStringFromDate(trip.arrival, dateStyle: .NoStyle, timeStyle: .ShortStyle) let durationFormatter = NSDateComponentsFormatter() durationFormatter.allowedUnits = [.Hour, .Minute] durationFormatter.unitsStyle = .Short durationLabel.text = durationFormatter.stringFromDate(trip.departure, toDate: trip.arrival) let delay = trip.actualDeparture.timeIntervalSinceDate(trip.departure) bei Verzögerung > 0 { durationFormatter.unitsStyle = .Full delayLabel.text = String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Zeige die Verzögerung"), durationFormatter.stringFromTimeInterval(delay)!) departureTimeLabel.textColor = .redColor() } sonst { delayLabel.hidden = true } } } [/code] Glücklicherweise verwenden wir ein Storyboard mit Einschränkungen und brauchen keinen Code, um eine der Ansichten zu positionieren. Und wenn das alles wäre, müssten wir wahrscheinlich auch nichts ändern, da dies immer noch klein genug ist, um es zu pflegen. Aber in echten Anwendungen machen die View Controller viel mehr als das, und in diesem Fall ist es gut, die Dinge zu vereinfachen und die verschiedenen Anliegen zu trennen. Das wird auch die Testbarkeit erheblich verbessern.

Lösung 1: Verwenden Sie MVVM

Wir können unseren Code verbessern, indem wir das MVVM-Muster verwenden. Dies würde in etwa so aussehen: [code language="obj-c"] Klasse TripViewViewModel { let Datum: String let Abfahrt: String let Ankunft: String let Dauer: String let delay: String? let delayHidden: Bool let departureTimeColor: UIColor init(_ trip: Trip) { date = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .ShortStyle, timeStyle: .NoStyle) Abfahrt = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .NoStyle, timeStyle: .ShortStyle) arrival = NSDateFormatter.localizedStringFromDate(trip.arrival, dateStyle: .NoStyle, timeStyle: .ShortStyle) let durationFormatter = NSDateComponentsFormatter() durationFormatter.allowedUnits = [.Hour, .Minute] durationFormatter.unitsStyle = .Short duration = durationFormatter.stringFromDate(trip.departure, toDate: trip.arrival)! let delay = trip.actualDeparture.timeIntervalSinceDate(trip.departure) bei Verzögerung > 0 { durationFormatter.unitsStyle = .Full self.delay = String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Zeige die Verzögerung"), durationFormatter.stringFromTimeInterval(delay)!) departureTimeColor = .redColor() delayHidden = false } sonst { self.delay = nil departureTimeColor = UIColor(rot: 0, grün: 0, blau: 0.4, alpha: 1) delayHidden = true } } } [/code] Wir haben eine separate Klasse TripViewViewModel erstellt, die alles aus unserem Trip-Modell in Dinge umwandelt, die wir in unserer Ansicht verwenden können. Beachten Sie, dass diese neue Klasse nichts über UIView-Klassen wie UILabelsusw. weiß. Diese Bindung wird nach wie vor in unserem View Controller vorgenommen, der jetzt wie folgt aussieht: [code language="obj-c"] class ViewController: UIViewController { @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var departureTimeLabel: UILabel! @IBOutlet weak var arrivalTimeLabel: UILabel! @IBOutlet weak var durationLabel: UILabel! @IBOutlet weak var delayLabel: UILabel! let tripModel = TripViewViewModel(Trip(Abfahrt: NSDate(timeIntervalSince1970: 1444396193), arrival: NSDate(timeIntervalSince1970: 1444397193), actualDeparture: NSDate(timeIntervalSince1970: 1444396493))) override func viewDidLoad() { super.viewDidLoad() dateLabel.text = tripModel.date departureTimeLabel.text = tripModel.departure arrivalTimeLabel.text = tripModel.arrival durationLabel.text = tripModel.duration delayLabel.text = tripModel.delay delayLabel.hidden = tripModel.delayHidden departureTimeLabel.textColor = tripModel.departureTimeColor } } [/code] Da es sich jetzt um eine Eins-zu-Eins-Bindung von MVVM-Eigenschaften an UIView-Eigenschaften (in diesem Fall UILabel) handelt, hat sich die Komplexität unseres View-Controllers erheblich verringert. Und unser TripViewViewModel ist wirklich einfach zu testen.

Verschieben Sie die Logik in das Modell

Unsere derzeitige Klasse TripViewViewModel enthält einige Logik, die besser in unser Trip-Modell passen könnte. Insbesondere alle Berechnungen, die nicht direkt mit der Präsentation zusammenhängen. [code language="obj-c"] class Trip { let departure: NSDate let arrival: NSDate let actualDeparture: NSDate let delay: NSTimeInterval let delayed: Bool let duration: NSTimeInterval init(departure: NSDate, arrival: NSDate, actualDeparture: NSDate? = nil) { self.departure = departure self.arrival = arrival self.actualDeparture = actualDeparture ?? departure // Berechnungen duration = self.arrival.timeIntervalSinceDate(self.departure) delay = self.actualDeparture.timeIntervalSinceDate(self.departure) delayed = delay > 0 } } [/code] Hier haben wir die Logik zur Berechnung der Dauer und der Verzögerung in das Trip-Modell verschoben. Die Präsentationslogik verbleibt in unserem TripViewViewModel, das nun die berechneten Eigenschaften des Modells verwendet: [code language="obj-c"] Klasse TripViewViewModel { let Datum: String let Abfahrt: String let Ankunft: String let Dauer: String let delay: String? let delayHidden: Bool let departureTimeColor: UIColor init(_ trip: Trip) { date = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .ShortStyle, timeStyle: .NoStyle) Abfahrt = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .NoStyle, timeStyle: .ShortStyle) arrival = NSDateFormatter.localizedStringFromDate(trip.arrival, dateStyle: .NoStyle, timeStyle: .ShortStyle) let durationFormatter = NSDateComponentsFormatter() durationFormatter.allowedUnits = [.Hour, .Minute] durationFormatter.unitsStyle = .Short duration = durationFormatter.stringFromTimeInterval(trip.duration)! delayHidden = !trip.delayed if trip.delayed { durationFormatter.unitsStyle = .Full delay = String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Zeige die Verzögerung"), durationFormatter.stringFromTimeInterval(trip.delay)!) departureTimeColor = .redColor() } sonst { self.delay = nil departureTimeColor = UIColor(rot: 0, grün: 0, blau: 0.4, alpha: 1) } } } [/code] Da sich die Eigenschaften des TripViewViewModels nicht geändert haben, bleibt unser View Controller genau derselbe.

Lösung 2: Präsentationssteuerungen

Wenn es Ihnen wie mir geht, finden Sie die Einstellung bestimmter Eigenschaften wie delayHidden und departureTimeColor im TripViewViewModel vielleicht etwas seltsam. Diese werden nämlich einfach mit den Eigenschaften hidden und textColor von UILabelsübersetzt. Außerdem benötigen wir eine Menge zusätzlichen Code nur für das Kopieren von Eigenschaftswerten. Was können wir also tun, um die Präsentationslogik näher an unsere UIView-Klassen heranzuführen? Kleine Klassen, die die Bindung zwischen dem Modell und den Ansichten steuern, sind die Antwort: Präsentationssteuerungen. Der Hauptunterschied zwischen MVVM-Klassen und Presentation Controls besteht darin, dass letztere über UIView-Klassen Bescheid wissen. Presentation Controls sind wie Mini-View-Controller, die nur für die Präsentation zuständig sind. Sie können einen ähnlichen Effekt erzielen, indem Sie benutzerdefinierte UIView-Unterklassen erstellen, die eine Kombination von Views sind. Dies funktionierte recht gut mit Ansichten, die vollständig in Code geschrieben waren, oder mit Ansichten, die aus einer Nib-Datei erstellt wurden, aber es funktioniert überhaupt nicht gut in Kombination mit Storyboards. Wir wollen zwar einen Teil der Komplexität aus unserem View Controller entfernen, aber wir wollen unsere einzelnen Ansichten in einer Storyboard-Szene nicht in eine separate Nib-Datei verschieben. Was Sie vielleicht nicht wissen, ist, dass Sie Ausgänge von Ansichten in Ihrer Szene zu anderen Objekten als dem View Controller dieser Szene erstellen können. Sie können sie mit jedem Objekt verbinden, das Sie zu Ihrer Szene hinzufügen. Lassen Sie uns zunächst unsere Präsentationssteuerung erstellen, eine neue Klasse mit dem Namen TripPresentationControl: [code language="obj-c"] class TripPresentationControl: NSObject { } [/code] Vergewissern Sie sich, dass es sich um eine Unterklasse von NSObject handelt, da Sie sonst Probleme haben werden, es mit Interface Builder zu erstellen. Gehen Sie nun zur Objektbibliothek in Interface Builder und ziehen Sie ein neues Objekt in Ihre Szene.

Bildschirmfoto 2015-10-09 um 17.09.22

Ändern Sie seine Klasse im Identitätsinspektor in die Klasse des Präsentationssteuerelements: TripPresentationControl.

Bildschirmfoto 2015-10-09 um 17.13.21

Sie können diese Klasse nun fast genauso verwenden wie einen View Controller. Ziehen Sie einfach Ausgänge aus Ihrer Szene in diese Klasse. Auf diese Weise verschieben wir alle unsere Beschriftungen in die Präsentationssteuerung und verbinden sie mit der Szene. [code language="obj-c"] class TripPresentationControl: NSObject { @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var departureTimeLabel: UILabel! @IBOutlet weak var arrivalTimeLabel: UILabel! @IBOutlet weak var durationLabel: UILabel! @IBOutlet weak var delayLabel: UILabel! var trip: Trip! { didSet { dateLabel.text = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .ShortStyle, timeStyle: .NoStyle) departureTimeLabel.text = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .NoStyle, timeStyle: .ShortStyle) arrivalTimeLabel.text = NSDateFormatter.localizedStringFromDate(trip.arrival, dateStyle: .NoStyle, timeStyle: .ShortStyle) let durationFormatter = NSDateComponentsFormatter() durationFormatter.allowedUnits = [.Hour, .Minute] durationFormatter.unitsStyle = .Short durationLabel.text = durationFormatter.stringFromTimeInterval(trip.duration)! delayLabel.hidden = !trip.delayed if trip.delayed { durationFormatter.unitsStyle = .Full delayLabel.text = String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Zeige die Verzögerung"), durationFormatter.stringFromTimeInterval(trip.delay)!) departureTimeLabel.textColor = .redColor() } } } } [/code] Natürlich können Sie mehrere Präsentationssteuerelemente erstellen, um komplexe View-Controller in überschaubare kleinere Steuerelemente aufzuteilen. Wie Sie sehen, haben wir auch eine Trip-Eigenschaft erstellt, die unsere gesamte Präsentationslogik enthält, ziemlich genau die gleiche, die wir in unserem TripViewViewModel hatten. Aber jetzt setzen wir direkt Werte für die Beschriftungen. Der nächste Schritt besteht darin, einen Ausgang von unserem Präsentationssteuerelement aus der Szene zum View Controller zu erstellen.

Steckdose

Beim Laden unserer Ansicht übergeben wir die Reise an die Präsentationssteuerung. Unser gesamter View-Controller sieht nun wie folgt aus: [code language="obj-c"] class ViewController: UIViewController { @IBOutlet var tripPresentationControl: TripPresentationControl! let trip = Trip(Abfahrt: NSDate(timeIntervalSince1970: 1444396193), arrival: NSDate(timeIntervalSince1970: 1444397193), actualDeparture: NSDate(timeIntervalSince1970: 1444396493)) override func viewDidLoad() { super.viewDidLoad() tripPresentationControl.trip = trip } } [/code] Ziemlich sauber, oder?

Und was ist mit MVVM?

Sollten Sie also aufhören, MVVM zu verwenden? Nicht unbedingt. MVVM und Presentation Controls haben beide ihre eigenen Stärken und Sie können sie sogar zusammen verwenden. Mit MVVM allein werden Sie wahrscheinlich etwas mehr Code schreiben als mit den Präsentationssteuerungen allein. Dadurch ist MVVM etwas einfacher zu testen, da Sie keine UIViewsinstanziieren müssen. Wenn Sie Presentation Controls testen wollen, müssen Sie die Views erstellen, aber das Tolle daran ist, dass dies einfache Dummy-Views sein können. Sie müssen nicht die gesamte View-Hierarchie replizieren oder Ihre View-Controller instanziieren. Und die meisten Eigenschaften, die Sie für die UIViewsfestlegen, können Sie einfach testen, wie z.B. den Text eines UILabels. In vielen großen Projekten können die MVVM-Klassen und die Presentation Controls perfekt zusammenarbeiten. In diesem Fall würden Sie das MVVM-Objekt an Ihr Presentation Control weitergeben. Das Presentation Control wäre dann die Schicht zwischen Ihren Ansichten und dem MVVM und seinem Model. Dinge wie Netzwerkanfragen werden auch viel besser in einem MVVM-Objekt gehandhabt, da Sie wahrscheinlich nicht die Komplexität von Netzwerkanfragen und Ansichten zusammen in einer Komponente haben möchten.

Fazit

Es hängt ganz von Ihnen und der Größe Ihres Projekts ab, ob Sie MVVM, Presentation Controls oder beides verwenden. Wenn Sie derzeit über View-Controller mit mehr als 1000 Zeilen Code verfügen, könnte es eine gute Idee sein, beide zu verwenden. Wenn Sie aber nur sehr einfache View-Controller haben, dann sollten Sie vielleicht bei den traditionellen View-Controller-Mustern bleiben. In naher Zukunft werde ich einen Folgebeitrag schreiben, in dem ich detaillierter darauf eingehe, wie man mit Modelländerungen und Benutzerinteraktion in Presentation Controls umgeht. Wenn Sie mehr über MVVM in iOS lesen möchten, empfehle ich Ihnen Introduction to MVVM von Ash Furrow und From MVC to MVVM in Swift von Srdan Rasic. Besuchen Sie auch unbedingt die do {iOS} Konferenz in Amsterdam am 9. November 2015. Hier wird Natasha "the Robot" Murashev einen Vortrag über protokollorientierte MVVM halten. Wenn Ihnen die Lektüre dieses Beitrags gefallen hat, werden Sie sich ihre Sitzung über MVVM auf jeden Fall ansehen wollen.

Verfasst von

Lammert Westerhoff

Contact

Let’s discuss how we can support your journey.