Als je een SwiftUI-listrij wijzigingen in een ObservableObject-item wilt laten weergeven, geef je dat item door aan een rijweergave met @ObservedObject en publiceer je de veranderlijke eigenschappen ervan. Deze handleiding bouwt een kleine Mac-labelbewerker en licht de afzonderlijke rollen toe van collectie-updates, rijobservatie en identiteit.
Waarom blijft de rij ongewijzigd wanneer een item verandert?
Neem een store met een eigenschap @Published var items: [LabelItem], waarbij elk LabelItem een klasse is. Door een item toe te voegen, verandert de array. Als je een bestaande item een nieuwe titel toewijst, verandert het object waarnaar wordt verwezen. Dat zijn verschillende mutaties: Swift-arrays zijn waardetypen, terwijl klasse-instanties referentiesemantiek hebben. Een array met klassenreferenties bevat nog steeds verwijzingen naar gedeelde instanties. Waarde- en referentietypen in Swift.
In de praktijk betekent dit dat het publiceren van de array niet automatisch observatie instelt voor elke eigenschap van elk object in de array. Apple noemt het volgen van collecties objecten als een voordeel van het nieuwere Observation-systeem. Geef bij een bestaande ObservableObject-implementatie elke rij een eigen verbinding met het object dat deze weergeeft. Migreren van het Observable Object-protocol naar de Observable-macro.
Een handige manier om het probleem te diagnosticeren, is twee verantwoordelijkheden te onderscheiden: de omvattende weergave observeert welke items in de lijst staan; elke rij observeert de veranderlijke inhoud van het bijbehorende item. Apple beschrijft @ObservedObject als de wrapper voor een binnenkomend ObservableObject waarvan gepubliceerde wijzigingen de ontvangende weergave moeten bijwerken. ObservedObject.
Bouw een compleet macOS-voorbeeld met een geobserveerde rij
Gebruik deze versie als je app al ObservableObject van Combine gebruikt, ook wanneer de app macOS-versies ouder dan 14 ondersteunt. Het voorbeeld vereist macOS 11 of hoger, omdat de eigenaar @StateObject gebruikt, dat volgens Apple beschikbaar is vanaf macOS 11. Gebruik een actuele Swift-compiler in Xcode. StateObject.
Maak een macOS-appdoel met de SwiftUI-interface. Vervang het gegenereerde app-entrybestand door het volledige programma hieronder en verwijder de gegenereerde content view als je die niet meer nodig hebt. Zorg dat er precies één @main-declaratie in het doel staat. Alle voorbeeldlabels zijn synthetisch en alle interacties vinden plaats in het eigen venster van de app.
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()
}
}
}
De store beheert de iteminstanties. LabelListView maakt de store aan en bewaart deze met @StateObject; de rijen ontvangen bestaande instanties met @ObservedObject. Apple legt uit dat een state object gedurende de levensduur van de identiteit van de declarerende container behouden blijft, en dat een geobserveerd object normaal gesproken invoer is in plaats van een nieuw aangemaakte standaardwaarde. StateObject, ObservedObject.
Het tekstveld en de schakelaar gebruiken bindings naar de gepubliceerde eigenschappen van het item. De tekst ernaast toont dezelfde titel, zodat je kunt zien dat bewerken en weergeven hetzelfde model gebruiken. @MainActor zorgt ervoor dat de model- en interfacebewerkingen in dit voorbeeld op de main actor plaatsvinden; Apple beschrijft diens executor als gelijkwaardig aan de hoofdverzendwachtrij. ObservedObject, MainActor.
Dit programma is gecontroleerd aan de hand van de gedocumenteerde API's, maar is hier niet gecompileerd: in de beschikbare omgeving is geen Swift-toolchain aanwezig. Bouw en voer het uit in Xcode om het gedrag op je implementatiedoel te controleren.
Controleer itemwijzigingen los van collectieveranderingen
Volg deze stappen om de twee updatepaden van elkaar te onderscheiden. Dit zijn verwachte resultaten op basis van het programma, geen gerapporteerde testmetingen.
- Start de app. Er moeten twee rijen verschijnen, met aanvankelijk de labels
AmberenCobalt. - Klik op
Change first label. De eerste rij moetCoppertonen en de schakelaarEnabledmoet veranderen. Het aantal rijen moet twee blijven. - Bewerk het veld
Labelin de eerste rij. De tekst ernaast moet dezelfde titel weergeven. - Klik op
Add label. Er moet een derde rij met de naamNew labelverschijnen en het aantal rijen moet toenemen. - Klik op
Remove lasttotdat de lijst leeg is. De knoppen voor verwijderen en het eerste label moeten uitgeschakeld worden; een label toevoegen moet mogelijk blijven.
De tweede stap is bijzonder nuttig, omdat de mutatie ontstaat in de knopactie van de omvattende weergave. De actie verandert het bestaande item zonder de array te vervangen. Als de rij vervolgens wordt bijgewerkt, controleer je daarmee rechtstreeks het observatiepad van het item. De stappen voor toevoegen en verwijderen testen afzonderlijk de gepubliceerde array van de store.
Als toevoegen werkt maar titelwijzigingen niet, controleer dan de eigenschapsdeclaratie in de rij die de titel leest. In deze architectuur moet de rij het item ontvangen via @ObservedObject en moet de veranderlijke titel @Published zijn. Publiceren is gekoppeld aan de eigenschap met die wrapper; het is geen recursieve declaratie voor een volledige objectgraaf. Published, ObservedObject.
Diagnoseer de resterende problemen in een bruikbare volgorde
Controleer of de schrijver en de rij dezelfde instantie delen
Volg het object dat aan LabelRow(item:) wordt doorgegeven en het object dat door de actie wordt gewijzigd. Een andere LabelItem met dezelfde titel aanmaken levert een aparte instantie op. Ook het kopiëren van de titel naar lokale rijstatus maakt een extra waarde die je synchroon moet houden. Bind voor deze editor rechtstreeks aan het gedeelde item, tenzij je bewust een concept-en-bevestigworkflow nodig hebt. Dit volgt uit het gedocumenteerde onderscheid in Swift tussen waarden kopiëren en klassenreferenties delen. Waarde- en referentietypen in Swift.
Houd de identiteit stabiel terwijl de inhoud verandert
In het voorbeeld krijgt elk item een opgeslagen, onveranderlijke identifier. ForEach, dat aan de basis staat van veelgebruikte dynamische lijstconstructies, vereist identificeerbare elementen of een expliciete identifier. Dankzij identiteit kan SwiftUI gegenereerde inhoud tijdens updates aan het bijbehorende gegevenselement koppelen. Daarmee wordt de observatieverbinding van de rij niet vervangen. ForEach.
Gebruik niet routinematig een veranderende .id(UUID()) als mechanisme om de weergave te vernieuwen. Apple documenteert dat het wijzigen van de identiteit van een weergave de status ervan reset, waaronder de focusstatus. Een rij opnieuw aanmaken om een bewerkte titel weer te geven voegt een aparte wijziging in de levenscyclus toe aan een inhoudswijziging; stel eerst het observatiepad in. StateObject.
Behandel samenvattingen in de bovenliggende weergave als extra gebruikers
Stel dat je boven de lijst een telling van ingeschakelde items toevoegt. Bij het hier getoonde oudere ontwerp zorgt het observeren van LabelStore er niet automatisch voor dat de bovenliggende weergave de isEnabled-publisher van elk item observeert. Alleen de rijen herstellen legt de afhankelijkheden voor die samenvatting dus niet vast.
Kies bewust hoe de update werkt: aggregeer meldingen van onderliggende objecten in de store, gebruik waardetypen voor items waarvan wijzigingen teruggeschreven worden naar de gepubliceerde collectie, of stap over op Observation als je implementatiedoel dat toelaat. Dit zijn architectuurkeuzes die voortkomen uit het onderscheid tussen collectie en item; het voorbeeld implementeert alleen rechtstreekse rijobservatie. Apples migratiehandleiding beschrijft hoe het nieuwere systeem collecties kan volgen en de eigenschappen die een weergave daadwerkelijk leest. Migreren van het Observable Object-protocol naar de Observable-macro.
Houd bij het implementeren van aggregatie op basis van publishers rekening met het meldingstijdstip. Apple specificeert dat @Published publiceert tijdens willSet: een abonnee ontvangt de binnenkomende waarde voordat de opgeslagen eigenschap verandert. Als je die eigenschap binnen de callback opnieuw uitleest, kan dat de vorige waarde opleveren. Gebruik de uitgezonden waarde wanneer je de berekening op die gebeurtenis baseert. Published.
Wanneer stap je over op Observation op macOS 14 of hoger?
Voor een minimale implementatiedoelversie van macOS 14 of hoger ondersteunt SwiftUI het Observation-systeem. Met een @Observable-model houdt SwiftUI bij welke observeerbare eigenschappen tijdens de evaluatie van een weergave worden gelezen. Apples handleiding laat specifiek een collectie observeerbare objecten zien waarvan de afzonderlijk weergegeven titels worden bijgewerkt wanneer ze veranderen. Modelgegevens beheren in je app.
Werk voor de migratie van dit voorbeeld zowel LabelItem als LabelStore bij om @Observable met import Observation te gebruiken, verwijder hun conformiteit aan ObservableObject en verwijder @Published. Verander de store-eigenschap die het object beheert van @StateObject in @State. Omdat deze rij bewerkbare bedieningselementen bevat, verander je de wrapper voor het binnenkomende item van @ObservedObject in @Bindable. Deze wijzigingen volgen het door Apple gedocumenteerde migratiepad. Migreren van het Observable Object-protocol naar de Observable-macro.
Een rij die alleen gegevens weergeeft, kan een observeerbaar item ontvangen via een gewone opgeslagen eigenschap en de titel ervan lezen in body. @Bindable levert de bindings die nodig zijn voor bedieningselementen zoals TextField en Toggle; het heeft een ander doel dan @ObservedObject. Gebruik geen @ObservedObject voor een model dat alleen het nieuwe Observation-systeem gebruikt: de generieke beperking ervan vereist ObservableObject. Bindable, ObservedObject.
De keuze is eenvoudig: behoud de oplossing met een geobserveerde rij om een bestaand Combine-model te herstellen met het huidige bereik van implementatiedoelen; overweeg Observation wanneer macOS 14 als minimum aanvaardbaar is en afhankelijkheidsregistratie op eigenschapsniveau past bij de rest van de interface.
Wat verandert er als de rij een AppKit-tekstweergave bevat?
Het SwiftUI-voorbeeld gebruikt standaardbedieningselementen en heeft geen aangepaste TextKit-integratie nodig. Als je eigen rij al een AppKit-weergave omhult met NSViewRepresentable, controleer dan na bevestiging van modelobservatie nog een tweede grens: Apple schrijft voor dat je de configuratie van de AppKit-weergave bijwerkt in updateNSView(_:context:) wanneer de bijbehorende SwiftUI-status verandert. De native weergave één keer aanmaken zorgt niet voor latere configuratie-updates. updateNSView(_:context:).
Maak voor een ingesloten NSTextView onderscheid tussen die synchronisatie van model naar weergave en tekstopmaak. Apple beschrijft NSTextView als de front-end van het AppKit-tekstsystem en documenteert een compatibiliteitsschakelaar wanneer de eigenschap layoutManager op macOS 12 of hoger expliciet wordt benaderd. Introduceer geen wijzigingen in de layout-engine tijdens het diagnosticeren van een ontbrekende rijobservatieverbinding. NSTextView.
Voor de labelbewerker is de concrete oplossing bij de rij rond: behoud de store, publiceer de bewerkbare eigenschappen van items en observeer elk binnenkomend item waar het wordt weergegeven. Controleer een itemmutatie zonder de collectie te veranderen en controleer daarna het toevoegen en verwijderen. Zo wordt duidelijk welke verbinding aandacht nodig heeft.
Bronnen
- Waarde- en referentietypen in Swift — Swift.org
- ObservedObject — Apple Developer Documentation
- StateObject — Apple Developer Documentation
- Published — Apple Developer Documentation
- ForEach — Apple Developer Documentation
- MainActor — Apple Developer Documentation
- Modelgegevens beheren in je app — Apple Developer Documentation
- Migreren van het Observable Object-protocol naar de Observable-macro — Apple Developer Documentation
- Bindable — Apple Developer Documentation
- updateNSView(_:context:) — Apple Developer Documentation
- NSTextView — Apple Developer Documentation