Aby wiersz listy SwiftUI odzwierciedlał edycje obiektu ObservableObject, przekaż ten obiekt do widoku wiersza za pomocą @ObservedObject i oznacz jego modyfikowalne właściwości jako publikowane. W tym poradniku zbudujemy prosty edytor etykiet na Maca i wyjaśnimy odrębne role aktualizacji kolekcji, obserwowania wierszy oraz tożsamości.
Dlaczego zmiana elementu nie aktualizuje wiersza?
Rozważmy magazyn z właściwością @Published var items: [LabelItem], w którym każdy LabelItem jest klasą. Dodanie elementu zmienia tablicę. Przypisanie nowego tytułu do istniejącego elementu zmienia wskazywany obiekt. To różne modyfikacje: tablice w Swift są typami wartości, natomiast instancje klas mają semantykę referencji. Tablica referencji do klas nadal zawiera referencje do współdzielonych instancji. Typy wartości i typy referencyjne w Swift.
Praktyczny skutek jest taki, że publikowanie tablicy nie powoduje automatycznie obserwowania każdej właściwości każdego zawartego w niej obiektu. W przewodniku migracji Apple wskazuje śledzenie kolekcji obiektów jako zaletę nowszego systemu Observation. W istniejącej implementacji ObservableObject zapewnij każdemu wierszowi własne połączenie z wyświetlanym obiektem. Migracja z protokołu Observable Object do makra Observable.
Przydatnym sposobem diagnozowania problemu jest rozpoznanie dwóch obowiązków: widok nadrzędny obserwuje, które elementy należą do listy, a każdy wiersz obserwuje zmienną zawartość swojego elementu. Apple opisuje @ObservedObject jako wrapper dla przekazywanego obiektu ObservableObject, którego publikowane zmiany powinny aktualizować widok odbierający. ObservedObject.
Zbuduj kompletny przykład macOS z obserwowanym wierszem
Użyj tej wersji, jeśli Twoja aplikacja korzysta już z ObservableObject z Combine, także gdy obsługuje wersje macOS wcześniejsze niż 14. Przykład wymaga macOS 11 lub nowszego, ponieważ jego właściciel używa @StateObject, który według dokumentacji Apple jest dostępny od macOS 11. Użyj aktualnego kompilatora Swift z Xcode. StateObject.
Utwórz cel aplikacji macOS z interfejsem SwiftUI. Zastąp wygenerowany plik wejściowy aplikacji poniższym kompletnym programem, a wygenerowany widok zawartości usuń, jeśli nie jest już potrzebny. W celu może znajdować się dokładnie jedna deklaracja @main. Wszystkie etykiety w przykładzie są fikcyjne, a wszystkie interakcje odbywają się we własnym oknie aplikacji.
import SwiftUI
import Combine
import Foundation
@MainActor
final class LabelItem: ObservableObject, Identifiable {
let id = UUID()
@Published var title: String
@Published var isEnabled: Bool
init(title: String, isEnabled: Bool = true) {
self.title = title
self.isEnabled = isEnabled
}
}
@MainActor
final class LabelStore: ObservableObject {
@Published var items: [LabelItem] = [
LabelItem(title: "Amber"),
LabelItem(title: "Cobalt", isEnabled: false)
]
func reviseFirstItem() {
guard let first = items.first else { return }
first.title = first.title == "Amber" ? "Copper" : "Amber"
first.isEnabled.toggle()
}
func addItem() {
items.append(LabelItem(title: "New label"))
}
func removeLastItem() {
guard !items.isEmpty else { return }
items.removeLast()
}
}
@MainActor
struct LabelRow: View {
@ObservedObject var item: LabelItem
var body: some View {
HStack {
TextField("Label", text: $item.title)
.textFieldStyle(.roundedBorder)
.frame(minWidth: 120)
Text(item.title)
.frame(minWidth: 100, alignment: .leading)
Toggle("Enabled", isOn: $item.isEnabled)
}
.padding(.vertical, 4)
}
}
@MainActor
struct LabelListView: View {
@StateObject private var store = LabelStore()
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Button("Change first label") {
store.reviseFirstItem()
}
.disabled(store.items.isEmpty)
Button("Add label") {
store.addItem()
}
Button("Remove last") {
store.removeLastItem()
}
.disabled(store.items.isEmpty)
}
Text("Rows: \(store.items.count)")
List(store.items) { item in
LabelRow(item: item)
}
}
.padding()
.frame(minWidth: 540, minHeight: 300)
}
}
@main
@MainActor
struct LabelRowsApp: App {
var body: some Scene {
WindowGroup {
LabelListView()
}
}
}
Magazyn jest właścicielem instancji elementów. LabelListView tworzy magazyn i przechowuje go za pomocą @StateObject; wiersze otrzymują istniejące instancje przez @ObservedObject. Apple wyjaśnia, że obiekt stanu zachowuje się przez czas życia tożsamości kontenera, który go deklaruje, a obserwowany obiekt powinien zwykle być wejściem, a nie nowo tworzonym domyślnym elementem. StateObject, ObservedObject.
Pole tekstowe i przełącznik używają powiązań z publikowanymi właściwościami elementu. Sąsiadujący tekst wyświetla ten sam tytuł, dzięki czemu można sprawdzić, czy edycja i wyświetlanie korzystają z jednego modelu. @MainActor utrzymuje operacje modelu i interfejsu w tym przykładzie na głównym aktorze, którego executor Apple opisuje jako równoważny głównej kolejce wysyłania. ObservedObject, MainActor.
Ten program sprawdzono pod kątem zgodności z udokumentowanymi API, ale nie został tutaj skompilowany: dostępne środowisko nie ma toolchaina Swift. Zbuduj i uruchom go w Xcode, aby zweryfikować działanie dla docelowej wersji systemu.
Sprawdź osobno zmiany elementów i zmiany kolekcji
Wykonaj poniższe czynności, aby rozróżnić te dwie ścieżki aktualizacji. Są to oczekiwane rezultaty wynikające z programu, a nie zgłoszone wyniki testów.
- Uruchom aplikację. Powinny być widoczne dwa wiersze, początkowo oznaczone
AmberiCobalt. - Kliknij
Change first label. W pierwszym wierszu powinien pojawić się tekstCopper, a przełącznikEnabledpowinien zmienić stan. Liczba wierszy powinna pozostać równa dwa. - Edytuj pole
Labelw pierwszym wierszu. Sąsiadujący tekst powinien odzwierciedlać ten sam tytuł. - Kliknij
Add label. Powinien pojawić się trzeci wiersz o nazwieNew label, a liczba wierszy powinna wzrosnąć. - Klikaj
Remove last, aż lista będzie pusta. Przyciski usuwania i zmiany pierwszej etykiety powinny zostać wyłączone; dodawanie etykiety powinno nadal być dostępne.
Drugi krok jest szczególnie przydatny, ponieważ modyfikacja zaczyna się w akcji przycisku widoku nadrzędnego. Zmienia ona istniejący element bez zastępowania tablicy. Prawidłowa aktualizacja wiersza sprawdza więc bezpośrednio ścieżkę obserwacji elementu. Kroki wstawiania i usuwania oddzielnie sprawdzają publikowaną tablicę magazynu.
Jeśli wstawianie działa, ale zmiany tytułu nie, sprawdź deklarację właściwości w wierszu, który odczytuje tytuł. W tej architekturze musi on otrzymywać element przez @ObservedObject, a zmienny tytuł musi być oznaczony jako @Published. Publikowanie dotyczy właściwości oznaczonej tym wrapperem; nie jest rekurencyjną deklaracją dla całego grafu obiektów. Published, ObservedObject.
Diagnozuj pozostałe problemy w użytecznej kolejności
Upewnij się, że zapisujący i wiersz współdzielą tę samą instancję
Prześledź obiekt przekazany do LabelRow(item:) oraz obiekt modyfikowany przez akcję. Utworzenie kolejnego LabelItem z takim samym tytułem tworzy osobną instancję. Podobnie skopiowanie tytułu do lokalnego stanu wiersza tworzy kolejną wartość, którą trzeba synchronizować. W tym edytorze powiąż element bezpośrednio ze współdzielonym obiektem, chyba że potrzebny jest celowy proces tworzenia wersji roboczej i zatwierdzania. Wynika to z udokumentowanego przez Swift rozróżnienia między kopiowaniem wartości a współdzieleniem referencji do klas. Typy wartości i typy referencyjne w Swift.
Zachowaj stabilną tożsamość podczas zmian zawartości
Przykład przypisuje każdemu elementowi zapisaną, niezmienną identyfikację. ForEach, który stanowi podstawę typowego tworzenia list dynamicznych, wymaga elementów identyfikowalnych albo jawnego identyfikatora. Tożsamość pozwala SwiftUI powiązać generowaną zawartość z odpowiadającym jej elementem danych podczas aktualizacji. Nie zastępuje ona połączenia obserwacji wiersza. ForEach.
Unikaj dodawania zmiennego .id(UUID()) jako standardowego mechanizmu odświeżania. Apple dokumentuje, że zmiana tożsamości widoku resetuje jego stan, w tym stan fokusu. Odtwarzanie wiersza w celu wyświetlenia edytowanego tytułu powoduje dodatkową zmianę cyklu życia przy zmianie zawartości; najpierw skonfiguruj ścieżkę obserwacji. StateObject.
Traktuj podsumowania nadrzędne jako dodatkowych odbiorców
Załóżmy, że nad listą dodajesz liczbę włączonych elementów. W pokazanym tu starszym rozwiązaniu obserwowanie LabelStore nie sprawia automatycznie, że widok nadrzędny obserwuje wydawcę isEnabled każdego elementu. Sama naprawa wierszy nie tworzy więc zależności potrzebnych temu podsumowaniu.
Jawnie wybierz sposób aktualizacji: agreguj powiadomienia potomków w magazynie, użyj elementów typu wartości, których edycje zapisują zmiany z powrotem do publikowanej kolekcji, albo zastosuj Observation, jeśli pozwala na to docelowa wersja systemu. Są to opcje architektoniczne wynikające z rozróżnienia między kolekcją a elementem; przykład implementuje tylko bezpośrednie obserwowanie wierszy. Przewodnik migracji Apple opisuje zdolność nowszego systemu do śledzenia kolekcji i właściwości faktycznie odczytywanych przez widok. Migracja z protokołu Observable Object do makra Observable.
Jeśli implementujesz agregację opartą na wydawcy, uwzględnij moment wysłania powiadomienia. Apple określa, że @Published publikuje podczas willSet: subskrybent otrzymuje nadchodzącą wartość przed zmianą zapisanej właściwości. Ponowne odczytanie właściwości wewnątrz tego wywołania zwrotnego może zwrócić jej poprzednią wartość. Przy obliczeniach opartych na tym zdarzeniu użyj wyemitowanej wartości. Published.
Kiedy przejść na Observation w macOS 14 lub nowszym?
Jeśli minimalną docelową wersją systemu jest macOS 14 lub nowszy, SwiftUI obsługuje system Observation. W przypadku modelu @Observable SwiftUI śledzi odczyty obserwowalnych właściwości podczas ewaluacji widoku. W przewodniku Apple pokazano konkretnie kolekcję obserwowalnych obiektów, w której tytuły wyświetlane przez poszczególne elementy aktualizują się po zmianie. Zarządzanie danymi modelu w aplikacji.
Aby zmigrować ten przykład, zmień zarówno LabelItem, jak i LabelStore, aby używały @Observable z import Observation, usuń ich zgodność z ObservableObject i usuń @Published. Zmień właściwość przechowującą magazyn z @StateObject na @State. Ponieważ ten wiersz zawiera edytowalne kontrolki, zmień wrapper przekazywanego elementu z @ObservedObject na @Bindable. Te zmiany odpowiadają udokumentowanej przez Apple ścieżce migracji. Migracja z protokołu Observable Object do makra Observable.
Wiersz wyświetlający tylko treść może otrzymać obserwowalny element przez zwykłą przechowywaną właściwość i odczytać jego tytuł w body. @Bindable zapewnia powiązania potrzebne kontrolkom takim jak TextField i Toggle; pełni inną funkcję niż @ObservedObject. Nie opakowuj w @ObservedObject modelu korzystającego wyłącznie z nowego systemu Observation, ponieważ jego ograniczenie generyczne wymaga ObservableObject. Bindable, ObservedObject.
Decyzja jest prosta: zachowaj rozwiązanie z obserwowanym wierszem, aby naprawić istniejący model Combine i obsługiwany przez niego zakres wersji systemu; rozważ Observation, jeśli macOS 14 jest akceptowalną minimalną wersją, a śledzenie zależności na poziomie właściwości pasuje do reszty interfejsu.
Co się zmienia, jeśli wiersz zawiera widok tekstowy AppKit?
Przykład SwiftUI używa standardowych kontrolek i nie wymaga niestandardowej integracji z TextKit. Jeśli Twój wiersz opakowuje już widok AppKit za pomocą NSViewRepresentable, po potwierdzeniu obserwacji modelu sprawdź jeszcze jedną granicę: Apple zaleca aktualizowanie konfiguracji widoku AppKit w updateNSView(_:context:), gdy zmienia się odpowiadający mu stan SwiftUI. Jednorazowe utworzenie widoku natywnego nie implementuje późniejszych aktualizacji konfiguracji. updateNSView(_:context:).
W przypadku osadzonego NSTextView odróżnij tę synchronizację modelu z widokiem od układu tekstu. Apple opisuje NSTextView jako część prezentacyjną systemu tekstowego AppKit i dokumentuje przełącznik zgodności, gdy jego właściwość layoutManager jest jawnie odczytywana w macOS 12 lub nowszym. Podczas diagnozowania braku połączenia obserwacji wiersza nie wprowadzaj zmian w silniku układu. NSTextView.
W edytorze etykiet konkretna poprawka na poziomie wiersza jest już gotowa: zachowaj magazyn, publikuj edytowalne właściwości elementów i obserwuj każdy przekazywany element w miejscu jego wyświetlania. Sprawdź modyfikację elementu bez zmiany kolekcji, a następnie sprawdź wstawianie i usuwanie. Ta sekwencja jasno wskazuje, któremu połączeniu trzeba się przyjrzeć.
Źródła
- Typy wartości i typy referencyjne w Swift — Swift.org
- ObservedObject — Dokumentacja deweloperska Apple
- StateObject — Dokumentacja deweloperska Apple
- Published — Dokumentacja deweloperska Apple
- ForEach — Dokumentacja deweloperska Apple
- MainActor — Dokumentacja deweloperska Apple
- Zarządzanie danymi modelu w aplikacji — Dokumentacja deweloperska Apple
- Migracja z protokołu Observable Object do makra Observable — Dokumentacja deweloperska Apple
- Bindable — Dokumentacja deweloperska Apple
- updateNSView(_:context:) — Dokumentacja deweloperska Apple
- NSTextView — Dokumentacja deweloperska Apple