Wenn es um die Integration eigens definierter Model-Klassen in SwiftUI-Views geht, spielen die beiden Property Wrapper ObservedObject
und StateObject
eine maßgebliche Rolle. Augenscheinlich machen sie mehr oder weniger das selbe: Sie achten auf Änderungen der Model-Instanz und aktualisieren die zugehörige View, sobald solche Änderungen bemerkt werden. Falls man daraus den Schluss zieht, man könne die beiden Property Wrapper beliebig nach eigenem Gusto einsetzen, der irrt. Ob man ObservedObject
oder StateObject
einsetzt, hängt maßgeblich davon ab, wie eine Model-Instanz in eine View integriert ist.
Zur Erläuterung der Unterschiede zwischen den beiden Property Wrappern dient die folgende Person
-Klasse als Ausgangslage. Sie wird in den kommenden Beispielen in verschiedene SwiftUI-Views integriert, um die Funktionsweise von ObservedObject
und StateObject
zu erläutern.
class Person: ObservableObject, Identifiable {
@Published var firstName: String
@Published var lastName: String
var fullName: String {
"\(firstName) \(lastName)"
}
init(firstName: String = "", lastName: String = "") {
self.firstName = firstName
self.lastName = lastName
}
}
ObservedObject
ObservedObject
ist dazu konzipiert, Model-Instanzen zu verwalten, die von außen an eine View übergeben werden. Die View benötigt in diesem Fall also eine entsprechende Model-Instanz, erstellt diese aber nicht selbst. Stattdessen wird sie der View bei ihrer Initialisierung als Parameter übergeben.
Ein typisches Beispiel für den Einsatz von ObservedObject
skizziert das nachfolgende Listing. PersonView
dient zur Anzeige und Bearbeitung einer Person
-Instanz. PersonView
legt zu diesem Zweck aber nicht selbst fest, welche Person
-Instanz zu bearbeiten ist. Stattdessen wird ihr eine solche aus einer List
-View heraus zugewiesen, in der vorhandene Personen aufgeführt sind. Abhängig von der gewählten Zelle erhält PersonView
die passende Person
-Instanz.
struct ContentView: View {
private var persons = [
Person(firstName: "Max", lastName: "Mustermann"),
Person(firstName: "Thomas", lastName: "Sillmann")
]
var body: some View {
NavigationStack {
List(persons) { person in
NavigationLink(person.fullName) {
PersonView(person: person)
}
}
}
}
}
struct PersonView: View {
@ObservedObject var person: Person
var body: some View {
Form {
TextField("First name", text: $person.firstName)
TextField("Last name", text: $person.lastName)
}
}
}
StateObject
Bei StateObject
liegt der Fall anders. StateObject
kommt in Views zum Einsatz, die selbst eine Model-Instanz erstellen und sie somit nicht von außen erhalten. Ein passendes Beispiel für StateObject
ist die nachfolgende CreatePersonView
. Sie dient dazu, eine neue Person
-Instanz zu erstellen, die bis dato noch gar nicht existiert. CreatePersonView
erzeugt diese neue Person
-Instanz und sorgt dank des StateObject
-Property Wrappers dafür, dass sich die Eigenschaften der Person bearbeiten lassen.
struct CreatePersonView: View {
@StateObject private var person = Person()
var body: some View {
Form {
Section {
TextField("First name", text: $person.firstName)
TextField("Last name", text: $person.lastName)
}
Section {
Button("Save") {}
}
}
}
}
Der feine Unterschied
Einfach zusammengefasst lässt sich daraus folgende Regel für den Einsatz von ObservedObject
und StateObject
ableiten: ObservedObject
erhält eine Model-Instanz von außen und definiert sie nicht selbst, während StateObject
umgekehrt eine neue Model-Instanz erzeugt.
Doch warum überhaupt diese Unterscheidung? Warum gab es die Notwendigkeit, StateObject
in der zweiten Iteration von SwiftUI neben ObservedObject
zu ergänzen?
Um diese Frage zu klären, soll das nachfolgende Beispiel als Ausgangslage dienen. Es zeigt eine ContentView
mit zwei Sections. Die erste enthält einen simplen Counter, der sich durch Druck auf einen Button hoch zählen lässt. Der aktuelle Wert ist in der State
-Property counter
gespeichert. Eine Änderung des Counters führt entsprechend zu einer Aktualisierung der View.
Zusätzlich gibt es noch eine PersonSection
, die als zweite Section innerhalb der Form
-View eingebunden ist. PersonSection
definiert eine neue Person
-Instanz, die sich über zwei Textfelder bearbeiten lässt. Entgegen der zuvor genannten Regel kommt zu Testzwecken für die Deklaration der person
-Property der ObservedObject
-Property Wrapper zum Einsatz (und nicht, wie es eigentlich sein sollte, StateObject
).
struct ContentView: View {
@State private var counter = 0
var body: some View {
Form {
Section {
Text("Counter: \(counter)")
Button("Count") {
counter += 1
}
}
PersonSection()
}
}
}
struct PersonSection: View {
@ObservedObject var person = Person()
var body: some View {
Section {
TextField("First name", text: $person.firstName)
TextField("Last name", text: $person.lastName)
}
}
}
Wenn man diesen Code einmal testet, wirkt er auf den ersten Blick korrekt und funktional. Der Counter lässt sich mithilfe des Buttons hoch zählen und Vor- und Nachname der Person lassen sich bearbeiten. Also alles bestens, oder? Keine Notwendigkeit, auf StateObject
zurückzugreifen.
Diese Annahme dürfte sich ändern, wenn man nach Bearbeitung von Vor- und/oder Nachname erneut den Counter-Button betätigt. Dann nämlich verschwinden die getätigten Eingaben für die Person und werden auf den Standard (sprich jeweils einen leeren String) zurückgesetzt.
Warum ist das so? ObservedObject
hält nicht die Referenz zu der zugewiesenen Model-Instanz. Wird nun PersonSection
aktualisiert (was im Falle der Counter-Erhöhung der Fall ist, da PersonSection
Teil der sich durch den Counter ändernden ContentView
ist), wird auch erneut die ObservedObject
-Zuweisung einer neuen Person
-Instanz ausgeführt. Die ursprüngliche Instanz geht verloren und wird durch die neue überschrieben.
Und genau hierin besteht der entscheidende Unterschied zu StateObject
. StateObject
hält die ihr zugewiesene Model-Instanz für den gesamten Lifecycle einer View. Auch bei einer von außen angestoßenen Aktualisierung wie im Falle des Counters behält StateObject
die Model-Instanz bei. Um das zu überprüfen, reicht es, für die person
-Property innerhalb von PersonSection
statt ObservedObject
den StateObject
-Property Wrapper zu verwenden:
struct PersonSection: View {
@StateObject var person = Person()
var body: some View {
Section {
TextField("First name", text: $person.firstName)
TextField("Last name", text: $person.lastName)
}
}
}
Fazit
Auch wenn ObservedObject
und StateObject
eine ähnliche Aufgabe erfüllen, sind sie für zwei unterschiedliche Anwendungsfälle konzipiert. ObservedObject
erhält eine Model-Instanz immer von außen, während StateObject
sie selbst erzeugt. Anhand dieser einfachen Regel kann man im Entwickler-Alltag auch fix entscheiden, welcher der beiden Property Wrapper der Richtige ist.
Warum ein „falscher“ Einsatz der Property Wrapper problematisch sein kann, zeigte das Counter-Beispiel. Ein ObservedObject
führt eine mögliche Wertzuweisung bei jeder View-Aktualisierung erneut aus, was zu ungewünschtem und fehlerhaften Verhalten führen kann. Nicht zuletzt aus diesem Grund wird Apple sich mit Version 2 von SwiftUI dazu entschieden haben, mit StateObject
einen alternativen Property Wrapper zur Verfügung zu stellen, der für genau jene Problematik eine passende Lösung bietet.
Euer Thomas
Schreibe einen Kommentar