Om du vill att en rad i en SwiftUI-lista ska återspegla ändringar i ett ObservableObject-objekt skickar du objektet till en radvy med @ObservedObject och publicerar dess ändringsbara egenskaper. Den här guiden bygger en liten etikettredigerare för Mac och förklarar de olika rollerna för uppdateringar av samlingen, observation av rader och identitet.

Varför förblir raden oförändrad när ett objekt ändras?

Tänk dig en lagringsinstans med en egenskap @Published var items: [LabelItem], där varje LabelItem är en klass. När du lägger till ett objekt ändras arrayen. När du tilldelar en befintlig instans en ny titel ändras det refererade objektet. Det är olika ändringar: Swift-arrayer är värdetyper, medan klassinstanser har referenssemantik. En array med klassreferenser innehåller fortfarande referenser till delade instanser. Värde- och referenstyper i Swift.

Den praktiska följden är att publicering av arrayen inte automatiskt skapar en observering av varje egenskap i varje objekt som ingår i den. Apples migreringsguide beskriver spårning av objektsamlingar som en fördel med det nyare Observation-systemet. Om du använder en befintlig ObservableObject-implementation får varje rad en egen koppling till objektet den visar. Migrera från Observable Object-protokollet till Observable-makrot.

Ett användbart sätt att felsöka problemet är att skilja mellan två ansvarsområden: den omslutande vyn observerar vilka objekt som ska ingå i listan, och varje rad observerar det ändringsbara innehållet i sitt objekt. Apple beskriver @ObservedObject som omslaget för ett inkommande ObservableObject vars publicerade ändringar ska uppdatera den mottagande vyn. ObservedObject.

Bygg ett komplett macOS-exempel med en observerad rad

Använd den här versionen när appen redan använder Combine-ramverkets ObservableObject, även om den har stöd för macOS-versioner före 14. Exemplet kräver macOS 11 eller senare eftersom den ägande vyn använder @StateObject, som Apple anger finns tillgängligt från macOS 11. Använd en aktuell Swift-kompilator i Xcode. StateObject.

Skapa ett macOS-appmål med SwiftUI-gränssnittet. Ersätt den genererade appens startfil med hela programmet nedan och ta bort den genererade innehållsvyn om du inte längre behöver den. Ha exakt en @main-deklaration i målet. Alla exempel på etiketter är syntetiska, och alla interaktioner sker i appens eget fönster.

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()
        }
    }
}

Lagringsinstansen äger objektinstanserna. LabelListView skapar och behåller lagringsinstansen med @StateObject; raderna tar emot befintliga instanser med @ObservedObject. Apple förklarar att ett tillståndsobjekt finns kvar så länge den deklarerande behållarens identitet består, och att ett observerat objekt normalt ska vara ett indataobjekt i stället för ett nyss skapat standardvärde. StateObject, ObservedObject.

Textfältet och växlingsknappen använder bindningar till objektets publicerade egenskaper. Texten bredvid visar samma titel, så att du enkelt kan kontrollera att redigering och visning använder samma modell. @MainActor håller modellens och gränssnittets åtgärder i det här exemplet på huvudaktören, vars exekverare Apple beskriver som likvärdig med huvudkön för dispatch. ObservedObject, MainActor.

Programmet har granskats mot de dokumenterade API:erna men kompilerades inte här, eftersom den tillgängliga miljön saknar en Swift-verktygskedja. Bygg och kör det i Xcode för att verifiera beteendet med ditt distributionsmål.

Kontrollera objektändringar separat från ändringar i samlingen

Följ den här sekvensen för att skilja mellan de två uppdateringsvägarna. Resultaten nedan är förväntade utifrån programmet, inte uppmätta testresultat.

  1. Starta appen. Den ska visa två rader med etiketterna Amber och Cobalt från början.
  2. Klicka på Change first label. Den första raden ska visa Copper, och dess Enabled-växlingsknapp ska ändras. Antalet rader ska vara två.
  3. Redigera fältet Label på den första raden. Texten bredvid ska visa samma titel.
  4. Klicka på Add label. En tredje rad med namnet New label ska visas, och antalet rader ska öka.
  5. Klicka på Remove last tills listan är tom. Knapparna för borttagning och ändring av den första etiketten ska inaktiveras; det ska fortfarande gå att lägga till en etikett.

Det andra steget är särskilt användbart eftersom ändringen görs i den omslutande vyns knappåtgärd. Den ändrar det befintliga objektet utan att ersätta arrayen. Om raden uppdateras har du därför kontrollerat objektets observationsväg direkt. Stegen för att lägga till och ta bort testar separat lagringsinstansens publicerade array.

Om det fungerar att lägga till objekt men inte att ändra titlar bör du granska egenskapsdeklarationen för den rad som läser titeln. I den här arkitekturen måste raden ta emot objektet via @ObservedObject, och den ändringsbara titeln måste vara @Published. Publiceringen gäller egenskapen som har det omslaget; den är ingen rekursiv deklaration för en hel objektgraf. Published, ObservedObject.

Felsök återstående problem i en användbar ordning

Kontrollera att den som ändrar och raden delar instans

Spåra objektet som skickas till LabelRow(item:) och objektet som ändras av åtgärden. Om du skapar ett nytt LabelItem med samma titel får du en separat instans. På samma sätt skapar du ett nytt värde som måste hållas synkroniserat om du kopierar titeln till lokalt radtillstånd. För den här redigeraren bör du binda direkt till det delade objektet, såvida du inte behöver ett avsiktligt arbetsflöde med utkast och bekräftelse. Det följer av Swifts dokumenterade skillnad mellan att kopiera värden och att dela klassreferenser. Värde- och referenstyper i Swift.

Behåll en stabil identitet medan innehållet ändras

I exemplet får varje objekt en lagrad, oföränderlig identifierare. ForEach, som ligger till grund för vanlig dynamisk listkonstruktion, kräver att elementen kan identifieras eller att en uttrycklig identifierare anges. Identiteten låter SwiftUI koppla genererat innehåll till motsvarande dataelement vid uppdateringar. Den ersätter inte radens observationskoppling. ForEach.

Undvik att rutinmässigt lägga till ett värde som ändras i .id(UUID()) för att tvinga fram uppdatering. Apple dokumenterar att en ändring av en vyidentitet återställer dess tillstånd, inklusive fokustillståndet. Om raden återskapas för att visa en ändrad titel innebär det en separat livscykeländring utöver innehållsändringen. Skapa först observationsvägen. StateObject.

Behandla sammanfattningar i den överordnade vyn som ytterligare konsumenter

Anta att du lägger till en räkning av aktiverade objekt ovanför listan. Med den äldre utformningen här gör observation av LabelStore inte automatiskt att den överordnade vyn observerar varje objekts isEnabled-publisher. Att bara åtgärda raderna skapar därför inte beroendena för sammanfattningen.

Välj uttryckligen hur den ska uppdateras: samla barnobjektens aviseringar i lagringsinstansen, använd värdetypsobjekt vars ändringar skrivs tillbaka via den publicerade samlingen eller inför Observation om distributionsmålet tillåter det. Det här är arkitekturalternativ som följer av skillnaden mellan samlings- och objektändringar; exemplet implementerar bara direkt observation av rader. Apples migreringsguide beskriver hur det nyare systemet kan spåra samlingar och de egenskaper som en vy faktiskt läser. Migrera från Observable Object-protokollet till Observable-makrot.

Om du implementerar aggregering baserad på publishers bör du ta hänsyn till när aviseringen skickas. Apple anger att @Published publicerar under willSet: en prenumerant får det inkommande värdet innan den lagrade egenskapen ändras. Om du läser egenskapen igen i återanropsfunktionen kan du därför få det föregående värdet. Använd det emitterade värdet när du beräknar något utifrån den händelsen. Published.

När bör du byta till Observation i macOS 14 eller senare?

Om lägsta distributionsmål är macOS 14 eller senare har SwiftUI stöd för Observation-systemet. Med en @Observable-modell spårar SwiftUI läsningar av observerbara egenskaper vid utvärdering av vyn. I Apples guide visas specifikt en samling observerbara objekt där de titlar som visas uppdateras när de enskilda titlarna ändras. Hantera modelldata i appen.

Om du vill migrera exemplet ändrar du både LabelItem och LabelStore så att de använder @Observable med import Observation, tar bort deras ObservableObject-konformanser och tar bort @Published. Ändra lagringsinstansens ägande egenskap från @StateObject till @State. Eftersom den här raden innehåller redigerbara kontroller ändrar du det inkommande objektets omslag från @ObservedObject till @Bindable. De här ändringarna följer Apples dokumenterade migreringsväg. Migrera från Observable Object-protokollet till Observable-makrot.

En rad som bara visar information kan ta emot ett observerbart objekt via en vanlig lagrad egenskap och läsa dess titel i body. @Bindable tillhandahåller de bindningar som kontroller som TextField och Toggle behöver; det fyller en annan funktion än @ObservedObject. Omslut inte en modell som endast använder det nya Observation-systemet med @ObservedObject, eftersom dess generiska begränsning kräver ObservableObject. Bindable, ObservedObject.

Valet är enkelt: behåll lösningen med observerade rader om du vill åtgärda en befintlig Combine-modell med dess nuvarande distributionsintervall; överväg Observation när macOS 14 är en godtagbar lägsta version och spårning av beroenden på egenskapsnivå passar resten av gränssnittet.

Vad ändras om raden innehåller en AppKit-textvy?

SwiftUI-exemplet använder standardkontroller och behöver ingen anpassad TextKit-integration. Om din egen rad redan omsluter en AppKit-vy med NSViewRepresentable bör du kontrollera ytterligare en gräns när du har bekräftat modellobservationen: Apple anger att du ska uppdatera AppKit-vyns konfiguration i updateNSView(_:context:) när motsvarande SwiftUI-tillstånd ändras. Att bara skapa den inbyggda vyn implementerar inte senare konfigurationsuppdateringar. updateNSView(_:context:).

För en inbäddad NSTextView bör du skilja synkroniseringen från modell till vy från textlayouten. Apple beskriver NSTextView som frontänden för AppKits textsystem och dokumenterar en kompatibilitetsväxel när dess egenskap layoutManager uttryckligen används i macOS 12 eller senare. Undvik att ändra layoutmotorn när du felsöker en utebliven observationskoppling för en rad. NSTextView.

För etikettredigeraren är den konkreta åtgärden vid radgränsen klar: behåll lagringsinstansen, publicera objektens redigerbara egenskaper och observera varje inkommande objekt där det visas. Verifiera en objektändring utan att ändra samlingen och kontrollera sedan att det går att lägga till och ta bort objekt. Då blir det tydligt vilken koppling som behöver ses över.

Källor