Beheer gedeelde observable state in je SwiftUI App, geef daarna dezelfde instantie door aan beide scenes. Houd elk venster's concept binnen zijn root view. Hierdoor kan een menubalk-besturing elk venster bijwerken terwijl elk venster zijn eigen onvoltooide tekst behoudt.
Deze tutorial bouwt die opstelling voor een kleine Mac label-editor: meerdere vensters tonen één gedeeld label, elk venster heeft een onafhankelijk concept, en een menubalk-paneel kan het gedeelde label bewerken of een ander venster openen. Alle voorbeeldtekst behoort tot de app en gebruikt synthetische labels.
Beslis welke wijzigingen elk venster moeten bereiken
Begin met het verwachte gedrag van elke waarde. Een gedeeld label moet overal veranderen wanneer het bewerkt wordt. Een onvoltooid concept moet alleen veranderen in het venster waar iemand typt. Deze eisen bepalen waar de waarden thuishoren.
Apple documenteert dat elk venster dat gemaakt wordt door een WindowGroup onafhankelijke opslag heeft voor state geïnstantieerd binnen zijn view-hiërarchie. Apple's model-data-richtlijnen demonstreren daarentegen het bezitten van een observable referentie in de top-level App en het doorgeven ervan aan een view-hiërarchie. Het combineren van die patronen geeft dit eigendomsplan. WindowGroup, Modeldata in je app beheren.
| Waarde | Eigenaar in dit voorbeeld | Bedoeld gedrag |
|---|---|---|
| Gedeeld label | App, gebruikmakend van @State met een @Observable model |
Elk venster en het menubalk-paneel bewerken dezelfde waarde |
| Onvoltooid concept | Elk venster's root view, gebruikmakend van @State |
Typen blijft binnen dat venster |
| Menubalk-besturingen | Ontvangen het app-eigen model | Wijzigingen bereiken alle consumenten van die instantie |
| Venster-openactie | Lezen uit de omgeving van het menubalk-view | Opent een venster vanuit de geïdentificeerde groep |
Het verschil ligt in de instantie-eigenaar. Twee apart aangemaakte objecten dezelfde klassenaam geven maakt hun inhoud niet gedeeld. Evenmin creëert het plaatsen van een referentie naar één app-eigen object in verschillende vensters onafhankelijke kopieën van dat object. Daarom creëert dit voorbeeld het gedeelde model één keer op app-niveau.
Stel de macOS-basis in voordat je de scenes toevoegt
Gebruik een macOS SwiftUI app-project met een deployment target van macOS 14 of later en een Xcode-toolchain die Observation ondersteunt, zoals Xcode 15 of later. SwiftUI's Observation-integratie begint bij macOS 14. MenuBarExtra en de vensterstijl zijn beschikbaar vanaf macOS 13, dus Observation stelt de hogere basis voor deze implementatie. Modeldata in je app beheren, MenuBarExtra, window.
Het menubalk-paneel gebruikt .menuBarExtraStyle(.window) omdat het tekstvelden en een aangepaste layout bevat. Apple beschrijft deze stijl als een popover-achtig venster met besturingselementen die gestijld zijn zoals die in een normaal venster. Het is een aparte scene van de editor's WindowGroup. window.
Het voorbeeld isoleert ook expliciet zijn model- en UI-typen naar @MainActor. Apple definieert de main actor als een executor equivalent aan de main dispatch queue. Hier stammen alle mutaties van UI-besturingen; geen achtergrondwerk is vereist. MainActor.
Bouw het gedeelde model en onafhankelijke venster-concepten
Vervang het gegenereerde app-toegangspunt door de volgende complete single-file-voorbeeld. Houd maar één @main-declaratie in het target. Het voorbeeld bevat imports, model, views en initiële tekst. Het is geïnspecteerd tegen de gedocumenteerde API's, maar kon niet gecompileerd worden in deze omgeving omdat geen Swift-toolchain beschikbaar was.
import SwiftUI
import Observation
@MainActor
@Observable
final class LabelStore {
var sharedLabel = "Sample Alpha"
}
@main
@MainActor
struct LabelScopeApp: App {
@State private var store = LabelStore()
var body: some Scene {
WindowGroup("Label Editor", id: "label-editor") {
EditorWindow()
.environment(store)
}
MenuBarExtra("Label Controls", systemImage: "square.grid.2x2") {
LabelMenuPanel()
.environment(store)
}
.menuBarExtraStyle(.window)
}
}
@MainActor
struct EditorWindow: View {
@Environment(LabelStore.self) private var store
@State private var draft = "Sample Draft"
var body: some View {
VStack(alignment: .leading, spacing: 16) {
SharedLabelControls()
Divider()
TextField("This window's draft", text: $draft)
.textFieldStyle(.roundedBorder)
Button("Use Draft as Shared Label") {
store.sharedLabel = draft
}
Text("Publishing the draft keeps its local text intact.")
.font(.caption)
}
.padding(20)
.frame(minWidth: 420, minHeight: 220)
}
}
@MainActor
struct SharedLabelControls: View {
@Environment(LabelStore.self) private var store
var body: some View {
@Bindable var editableStore = store
VStack(alignment: .leading, spacing: 8) {
Text("Shared label: \(store.sharedLabel)")
TextField("Shared label", text: $editableStore.sharedLabel)
.textFieldStyle(.roundedBorder)
}
}
}
@MainActor
struct LabelMenuPanel: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
VStack(alignment: .leading, spacing: 16) {
SharedLabelControls()
Button("Open Another Editor") {
openWindow(id: "label-editor")
}
}
.padding(16)
.frame(width: 320)
}
}
Er zijn twee expliciete omgevinginjecties: één op de vensterinhoud en één op de menubalkinhoud. Beide ontvangen store. Verwacht niet dat een omgevingsmodifier aan een vensterinhoud een afhankelijkheid voorziet aan een broerscene. Apple documenteert omgevingsdeling door een view-hiërarchie; de twee injecties maken de afhankelijkheid van elke hiërarchie expliciet. Modeldata in je app beheren.
Gebruik @Bindable waar een besturing een binding nodig heeft
SharedLabelControls leest het model via @Environment(LabelStore.self). In de body voorziet de lokale @Bindable-variabele de geprojecteerde binding die TextField vereist. Het verwijst naar het bestaande observable object en creëert geen extra store. Apple documenteert dit exacte environment-naar-binding-patroon. Bindable.
Het conceptveld gebruikt daarentegen $draft, omdat de waarde al eigendom is van de vensterview's @State. De knop kopieert het huidige concept in het gedeelde label. Dat is een expliciete actie: latere wijzigingen aan het gedeelde label herschrijven geen van de twee venster-concepten. Dit zijn de gekozen bewerkingssemantieken van het voorbeeld.
Match de window group identifier exact
De menubalk-knop verkrijgt openWindow uit zijn eigen omgeving en roept het aan met "label-editor", overeenkomend met de groepsdeclaratie. Apple documenteert deze identifier-gebaseerde benadering voor het maken van een venster vanuit een groep. De knop's doel is een andere editor te openen, dus dit voorbeeld belooft niet een bepaalde bestaande venster te lokaliseren en her te gebruiken. WindowGroup.
Verifieer het bereik met een twee-venster-sequentie
De volgende sequentie is een verwacht-gedragscheck voor het voorbeeld. Het onderscheidt gedeelde model-identiteit van onafhankelijke conceptopslag zonder extra instrumentatie nodig te hebben.
- Start de app. De eerste editor toont het gedeelde label
Sample Alphaen conceptSample Draft. - Open het menubalk-paneel en kies
Open Another Editor. - Verander het eerste venster's concept naar
Draft One. Laat het tweede venster's concept ongewijzigd. - In het menubalk-paneel, verander het gedeelde label naar
Sample Beta. Beide editor-vensters zouden dat gedeelde label moeten tonen, terwijl ze hun verschillende conceptwaarden behouden. - In het eerste venster, kies
Use Draft as Shared Label. Het gedeelde label wordtDraft Onein beide vensters en het menubalk-paneel. Het tweede venster's concept blijftSample Draft.
Deze sequentie controleert drie verschillende paden: lokaal typen, een menubalk-bewerking, en een expliciete lokale-naar-gedeelde kopie. Als lokaal typen het andere venster's concept verandert, inspecteer of het concept verplaatst was naar de app-eigen store. Als menubalk-bewerkingen de vensters niet bereiken, inspecteer of een andere LabelStore() aangemaakt was in de menubalk-hiërarchie.
| Symptoom | Eerste te inspecteren |
|---|---|
| Venster- en menubalk-labels divergeren | Afzonderlijke model-initialisaties in plaats van één gedeelde instantie |
| Elk concept verandert samen | Conceptopslag geplaatst in een gedeeld model |
| Een view faalt bij het lezen van zijn model | Ontbrekende .environment(store) in die hiërarchie |
| Een editor reset onverwacht | Wijzigingen aan de eigenaar-view's identiteit of vervanging van het model |
| Openen van een andere editor mislukt | Mismatch tussen gedeclareerde en aangevraagde scene-identifiers |
Het ontbrekende-omgeving-geval heeft een gedocumenteerd gevolg: het lezen van een niet-optioneel observable object door type veronderstelt dat een voorouder het voorzag; Apple zegt dat een afwezig object een exception veroorzaakt. Voorsien de afhankelijkheid in previews en alternatieve toegangspunten ook. Modeldata in je app beheren.
Het voorbeeld bewaart alles in geheugen en bevat geen opslag- of herstelafhandeling. Een venster sluiten zou daarom behandeld moeten worden als het verwerpen van het onvoltooide concept, en afsluiten als het verwerpen van het gedeelde label. Als concepten die acties moeten overleven, definieer dan een aparte persistente-eis voordat je dit voorbeeld uitbreidt.
Houd gefocuste commando's gescheiden van gedeelde menubalk-besturingen
Een gedeeld label heeft geen actieve-vensterselectie nodig: elke consument ontvangt hetzelfde model. Een actie die het momenteel gefocuste venster's concept bewerkt heeft een ander vereiste omdat het een bepaald venster moet identificeren.
Voor standaard applicatiemenu-commando's, documenteert Apple het publiceren van een venster-eigen model met focusedSceneValue en het consumeren ervan via @FocusedValue in een Commands-implementatie. Het voorbeeld schakelt het commando uit wanneer geen gefocust model bestaat. Dat is een ondersteund pad voor acties wier doel de actieve scene volgt. De menubalk bouwen en aanpassen met SwiftUI.
Concludeer niet van dat commando-voorbeeld dat het openen van een MenuBarExtra het een betrouwbare referentie geeft tot de eerder actieve editor. Dit artikel's paneel bedraagt bewust op app-wijde state. Als het paneel een individuele editor moet targeten, voeg dan een expliciet target-selectie-ontwerp toe, zoals een lijst van app-eigen editor-identifiers, in plaats van aan te nemen welk venster de actie zou moeten ontvangen.
Voeg AppKit of TextKit toe alleen wanneer de editor ze vereist
De label-editor heeft alleen SwiftUI-besturingen nodig. Als de eisen groeien om een aangepaste AppKit-tekstview te omvatten, is NSViewRepresentable Apple's ondersteunde brug voor het maken, bijwerken en afbreken van een NSView in een SwiftUI-hiërarchie. Apple beschrijft ook het gebruik van een coordinator om delegate- en target-action-berichten door te sturen naar SwiftUI. NSViewRepresentable.
Behoud het eigendomsplan bij het toevoegen van die brug: houd elke editor's view-instantie en bewerkingsstate geassocieerd met zijn venster, en geef gedeelde inhoud alleen door via het app-model waar deling beoogd is. Een coordinator moet bewerkingen doorsturen via een binding of model-actie. Als implementatie-voorzorgsmaatregel, vergelijk inkomende inhoud voordat je de tekstview's inhoud vervangt, zodat ongerelateerde gedeelde updates een gaande bewerking niet herhaaldelijk resetten.
Voor een TextKit 2-implementatie, identificeert Apple textLayoutManager als de moderne layout-engine-toegangspunt. De TextKit-sessie legt uit dat het openen van de legacy layoutManager op een TextKit 2-tekstview compatibiliteitsmodus kan activeren, waarbij die view naar TextKit 1 schakelt zonder automatische terugschakeling. Vermijd het gebruik van die legacy-property slechts om een tekstview te inspecteren. TextKit, Wat is er nieuw in TextKit en tekstviews.
Als macOS 13-ondersteuning vereist is, behoud dan dezelfde eigendomsregeling maar gebruik het oudere observatiepad: ObservableObject met @Published, een bezittend @StateObject, en .environmentObject met ontvangende @EnvironmentObject-properties. Apple's migratiegids legt uit hoe die mechanismen overeenkomen met de nieuwere Observation-benadering. Het wijzigen van observatiesystemen moet de beoogde delingsgrenzen behouden. Migreren van het Observable Object-protocol naar de Observable macro.
Kies eigendom op basis van het gedrag dat iemand van de interface verwacht: één app-eigen instantie voor het gedeelde label, één venster-eigen waarde voor elk onvoltooid concept, en een expliciete actie die ze verbindt. Houd de twee-venster-sequentie als regressiecheck wanneer het menubalk-paneel of de editor groeit.
Bronnen
- Modeldata in je app beheren — Apple
- WindowGroup — Apple
- MenuBarExtra — Apple
- window — Apple
- MainActor — Apple
- Bindable — Apple
- De menubalk bouwen en aanpassen met SwiftUI — Apple
- NSViewRepresentable — Apple
- TextKit — Apple
- Wat is er nieuw in TextKit en tekstviews — Apple
- Migreren van het Observable Object-protocol naar de Observable macro — Apple