Lernziel
Nach dieser Lektion kannst du älteren SwiftUI-Code lesen, der ObservableObject, @Published, @StateObject, @ObservedObject und @EnvironmentObject verwendet. Du weißt, wie die alten Muster den neuen entsprechen und wie du alten Code Schritt für Schritt auf @Observable umstellst.
Das Problem: Tutorials aus einer anderen Zeit
@Observable gibt es seit iOS 17. SwiftUI selbst gibt es aber schon seit iOS 13. In den Jahren dazwischen sind unzählige Tutorials, Bücher, Stack-Overflow-Antworten und ganze Apps entstanden, die einen anderen Weg verwenden. Früher oder später triffst du auf Code wie diesen:
class ProfileModel: ObservableObject {
@Published var name = "Anna"
}
struct ProfileView: View {
@StateObject private var model = ProfileModel()
var body: some View {
Text(model.name)
}
}Dieser Code ist nicht falsch. Er funktioniert bis heute. Aber er sieht anders aus als das, was du gelernt hast, und hat ein paar eigene Fallstricke. In dieser Lektion lernst du, ihn zu lesen und bei Bedarf umzustellen. Schreiben wirst du neuen Code weiterhin mit @Observable.
Die alte Welt im Überblick
| Alt (bis iOS 16) | Neu (ab iOS 17) | Zweck |
|---|---|---|
class X: ObservableObject | @Observable class X | Klasse beobachtbar machen |
@Published var name | var name | Eigenschaft beobachten |
@StateObject private var x = X() | @State private var x = X() | Objekt besitzen |
@ObservedObject var x: X | let x: X bzw. @Bindable var x: X | Objekt bekommen |
.environmentObject(x) | .environment(x) | Objekt in die Umgebung legen |
@EnvironmentObject var x: X | @Environment(X.self) var x | Objekt aus der Umgebung holen |
Fast jede Zeile hat also eine direkte Entsprechung. Gehen wir sie durch.
ObservableObject und @Published
class SettingsModel: ObservableObject {
@Published var userName = ""
@Published var notificationsOn = true
var internalCounter = 0
}ObservableObjectist ein Protokoll, kein Makro. Die Klasse erfüllt es mit einem Doppelpunkt.@Publishedmarkiert jede Eigenschaft, deren Änderung die Oberfläche aktualisieren soll.- Eigenschaften ohne
@PublishedwieinternalCounterlösen keine Aktualisierung aus. Bei@Observableentspricht das@ObservationIgnored.
Fallstrick 1: @Published vergessen
Vergisst man bei ObservableObject ein @Published, ändert sich die Eigenschaft zwar, aber die Oberfläche merkt nichts davon. Kein Fehler, keine Warnung. Ein sehr häufiger Anfängerfehler, den es bei @Observable nicht mehr gibt.
Die drei Property Wrapper
In der alten Welt gab es drei Wrapper für Objekte, je nach Situation.
@StateObject: besitzen
struct SettingsView: View {
@StateObject private var model = SettingsModel()
// …
}Entspricht @State bei @Observable: Die View erzeugt das Objekt und hält es am Leben.
@ObservedObject: bekommen
struct SettingsForm: View {
@ObservedObject var model: SettingsModel
var body: some View {
TextField("Name", text: $model.userName)
}
}Die View bekommt das Objekt von außen und beobachtet es. $model.userName liefert ein Binding. In der neuen Welt entspricht das let zum Lesen oder @Bindable für Bindings.
Fallstrick 2: @ObservedObject mit Startwert
@ObservedObject var model = SettingsModel() // ⚠️ gefährlichDas kompiliert, aber @ObservedObject besitzt das Objekt nicht. Wird die View neu erzeugt, entsteht ein neues Objekt, und alle Daten sind weg. Das passiert scheinbar zufällig, etwa wenn sich etwas in der Eltern-View ändert. Dieser Fehler war jahrelang eine der häufigsten Fragen zu SwiftUI. Richtig ist @StateObject.
@EnvironmentObject: aus der Umgebung
// Bereitstellen
ContentView()
.environmentObject(settings)
// Verwenden
struct DeepView: View {
@EnvironmentObject var settings: SettingsModel
}Entspricht .environment(settings) und @Environment(SettingsModel.self). Und genau wie in der neuen Welt stürzt die App ab, wenn das Objekt fehlt.
Der wichtigste Unterschied: Wie genau wird beobachtet?
Neben der Schreibweise gibt es einen inhaltlichen Unterschied, der in großen Apps spürbar wird.
Ein ObservableObject hat nur ein Signal: „Ich habe mich geändert.“ Ändert sich irgendeine @Published-Eigenschaft, werden alle Views neu berechnet, die das Objekt beobachten, egal welche Eigenschaft sie lesen.
class Profile: ObservableObject {
@Published var name = "Anna"
@Published var followers = 0
}
struct NameView: View {
@ObservedObject var profile: Profile
var body: some View { Text(profile.name) }
}Ändert sich followers, wird NameView trotzdem neu berechnet, obwohl sie followers gar nicht anzeigt.
Mit @Observable (Lektion 3.1) passiert das nicht. Dort merkt sich SwiftUI für jede View, welche Eigenschaften sie wirklich liest.
Alten Code umstellen
Hast du ein bestehendes Projekt mit ObservableObject, kannst du es Schritt für Schritt umstellen. Für eine Klasse:
Vorher:
class CartModel: ObservableObject {
@Published var items: [Item] = []
@Published var couponCode = ""
private var cache: [String: Double] = [:]
}
struct ShopView: View {
@StateObject private var cart = CartModel()
var body: some View {
CartView(cart: cart)
.environmentObject(cart)
}
}
struct CartView: View {
@ObservedObject var cart: CartModel
var body: some View {
TextField("Gutschein", text: $cart.couponCode)
}
}
struct CheckoutButton: View {
@EnvironmentObject var cart: CartModel
var body: some View {
Text("\(cart.items.count) Artikel")
}
}Nachher:
@Observable
class CartModel {
var items: [Item] = []
var couponCode = ""
@ObservationIgnored private var cache: [String: Double] = [:]
}
struct ShopView: View {
@State private var cart = CartModel()
var body: some View {
CartView(cart: cart)
.environment(cart)
}
}
struct CartView: View {
@Bindable var cart: CartModel
var body: some View {
TextField("Gutschein", text: $cart.couponCode)
}
}
struct CheckoutButton: View {
@Environment(CartModel.self) private var cart
var body: some View {
Text("\(cart.items.count) Artikel")
}
}Die Schritte:
: ObservableObjectentfernen,@Observabledavor schreiben.- Alle
@Publishedentfernen. Eigenschaften, die nicht beobachtet werden sollen, bekommen@ObservationIgnored. @StateObject→@State.@ObservedObject→let, oder@Bindable, wenn$verwendet wird..environmentObject(x)→.environment(x).@EnvironmentObject var x: X→@Environment(X.self) private var x.
Xcode hilft dir dabei: Nach Schritt 1 und 2 melden sich alle Stellen, die noch die alten Wrapper verwenden.
Tipp
Du musst nicht alles auf einmal umstellen. Alte und neue Klassen können problemlos nebeneinander existieren. Stell eine Klasse nach der anderen um und teste dazwischen.
Wann du die alte Welt noch brauchst
Ein paar Situationen, in denen ObservableObject noch eine Rolle spielt:
- Ältere iOS-Versionen: Muss deine App auf iOS 16 oder früher laufen, gibt es kein
@Observable. - Combine:
@Publishederzeugt einen sogenannten Publisher aus dem Framework Combine. Code, der damit arbeitet, etwa$name.debounce(...), brauchtObservableObject. In neuem Code ersetzt man das meist durchasync/await(Lektion 3.6). - Bestehende Bibliotheken, die ihre Objekte noch als
ObservableObjectanbieten.
Typische Fehler beim Lesen
$model in alter und neuer Welt
In beiden Welten liefert $model.name ein Binding. In der alten Welt bekommst du es von @ObservedObject und @StateObject, in der neuen von @State und @Bindable. Nur bei let gibt es kein $.
Gemischte Wrapper
@Observable class Model { }
@StateObject private var model = Model()Fehlermeldung
Generic struct 'StateObject' requires that 'Model' conform to 'ObservableObject'
Neue Klassen mit neuen Wrappern, alte mit alten. Mischen geht nicht.
@Published in einer @Observable-Klasse
Das ergibt einen Fehler, weil das Makro die Eigenschaft selbst umschreibt. Entferne @Published.
FAQ
Anfänger
Was bedeutet das überhaupt?Muss ich die alte Welt können, um eine App zu bauen?
Nein. Für neue Apps reicht @Observable. Diese Lektion ist dafür da, dass du nicht verwirrt bist, wenn du Code aus dem Internet liest, und dass du ihn übersetzen kannst.
Woran erkenne ich auf einen Blick, ob Code alt oder neu ist?
Steht irgendwo ObservableObject, @Published, @StateObject, @ObservedObject oder @EnvironmentObject, ist es die alte Welt. Steht @Observable über einer Klasse, ist es die neue.
Fortgeschritten
Warum verhält es sich so?Gibt es auch @State mit einem ObservableObject?
Technisch ja, aber dann wird das Objekt nicht beobachtet. Die Oberfläche aktualisiert sich nicht. Das ist ein weiterer Grund, die Welten nicht zu mischen.
Was ist objectWillChange?
Der Publisher, den jedes ObservableObject hat. @Published ruft automatisch objectWillChange.send() auf, kurz bevor sich eine Eigenschaft ändert. In altem Code siehst du manchmal einen manuellen Aufruf, wenn sich etwas ändert, das nicht @Published ist.
Profi
Was passiert unter der Haube?Warum „will change“ und nicht „did change“? SwiftUI will vor der Änderung wissen, dass gleich eine kommt, um den alten Zustand für Animationen festzuhalten. Die eigentliche Neuberechnung erfolgt dann gesammelt beim nächsten Durchlauf der Oberfläche. Mehrere Änderungen hintereinander lösen so nur eine Neuberechnung aus.
Wie gehen @Observable und Combine zusammen?
Gar nicht direkt: @Observable erzeugt keine Publisher. Brauchst du einen Datenstrom für Änderungen, gibt es seit neueren Swift-Versionen Observations, eine asynchrone Sequenz, die mit for await gelesen wird. Für die meisten Fälle reicht aber .onChange(of:) in der View.
Übungen
Übung 1: Übersetzen
Schreib die rechte Spalte der Tabelle aus dem Gedächtnis auf: Was ist das neue Gegenstück zu ObservableObject, @Published, @StateObject, @ObservedObject, .environmentObject und @EnvironmentObject?
Übung 2: Umstellen
Stell diesen Code auf @Observable um:
class TimerModel: ObservableObject {
@Published var seconds = 0
@Published var isRunning = false
var startDate: Date?
}
struct TimerScreen: View {
@StateObject private var timer = TimerModel()
var body: some View {
VStack {
Text("\(timer.seconds) s")
TimerControls(timer: timer)
}
}
}
struct TimerControls: View {
@ObservedObject var timer: TimerModel
var body: some View {
Toggle("Läuft", isOn: $timer.isRunning)
}
}Lösung Übung 2Erst selbst probieren!
@Observable
class TimerModel {
var seconds = 0
var isRunning = false
@ObservationIgnored var startDate: Date?
}
struct TimerScreen: View {
@State private var timer = TimerModel()
var body: some View {
VStack {
Text("\(timer.seconds) s")
TimerControls(timer: timer)
}
}
}
struct TimerControls: View {
@Bindable var timer: TimerModel
var body: some View {
Toggle("Läuft", isOn: $timer.isRunning)
}
}startDate war nicht @Published und wird deshalb @ObservationIgnored. TimerControls braucht ein Binding, deshalb @Bindable statt let.
Übung 3: Fehler finden
Was ist an diesem alten Code problematisch, und wie würdest du ihn reparieren, einmal in der alten und einmal in der neuen Welt?
struct ListScreen: View {
@ObservedObject var store = ItemStore()
var body: some View { … }
}Lösung Übung 3Erst selbst probieren!
@ObservedObject mit Startwert besitzt das Objekt nicht. Bei jeder Neuerzeugung der View entsteht ein neuer, leerer Store. In der alten Welt: @StateObject private var store = ItemStore(). In der neuen: ItemStore mit @Observable und @State private var store = ItemStore().
Zusammenfassung
- Ältere SwiftUI-Codes verwenden
ObservableObjectmit@Published. @StateObjectbesitzt,@ObservedObjectbekommt,@EnvironmentObjectholt aus der Umgebung.- Jedes alte Muster hat ein neues Gegenstück mit
@Observable,@State,let/@Bindableund@Environment. ObservableObjectmeldet nur „irgendetwas hat sich geändert“,@Observableverfolgt jede Eigenschaft einzeln.- Umstellen geht schrittweise, eine Klasse nach der anderen.
- Neuen Code schreibst du mit
@Observable.
Spickzettel
// Alt // Neu
class M: ObservableObject { @Observable class M {
@Published var x = 0 var x = 0
} }
@StateObject private var m = M() @State private var m = M()
@ObservedObject var m: M let m: M / @Bindable var m: M
.environmentObject(m) .environment(m)
@EnvironmentObject var m: M @Environment(M.self) private var m