Äg det delade observerbara tillståndet i din SwiftUI-App och skicka sedan samma instans till båda scenerna. Behåll varje fönsters utkast i dess rotvy. Då kan en kontroll i menyraden uppdatera alla fönster samtidigt som varje fönster behåller sin egen ofärdiga text.

I den här handledningen bygger vi en sådan lösning för en enkel etikettredigerare för Mac: flera fönster visar en gemensam etikett, varje fönster har ett eget utkast och en panel i menyraden kan redigera den gemensamma etiketten eller öppna ett fönster till. All exempeltext tillhör appen och använder syntetiska etiketter.

Avgör vilka ändringar som ska nå alla fönster

Börja med att fastställa det förväntade beteendet för varje värde. En gemensam etikett ska ändras överallt när den redigeras. Ett ofärdigt utkast ska bara ändras i fönstret där någon skriver. Dessa krav avgör var värdena ska ligga.

Apple dokumenterar att varje fönster som skapas av en WindowGroup har separat lagring för tillstånd som instansieras i dess vyhierarki. Apples vägledning om modelldata visar däremot hur man äger en observerbar referens på den översta nivån i App och skickar den vidare till en vyhierarki. Genom att kombinera de mönstren får vi följande plan för ägarskap. WindowGroup, Hantera modelldata i din app.

Värde Ägare i det här exemplet Avsett beteende
Gemensam etikett App, med @State och en @Observable-modell Alla fönster och panelen i menyraden redigerar samma värde
Ofärdigt utkast Rotvyn i varje fönster, med @State Texten stannar i det fönstret
Kontroller i menyraden Tar emot appens modell Ändringar når alla som använder den instansen
Åtgärd för att öppna ett fönster Hämtas från menyradsvyns miljö Öppnar ett fönster från den identifierade gruppen

Skillnaden ligger i vem som äger instansen. Att ge två separat skapade objekt samma klassnamn innebär inte att deras innehåll delas. På samma sätt skapar en referens till ett och samma appägda objekt i flera fönster inga oberoende kopior av objektet. Därför skapar exemplet den gemensamma modellen en gång på appnivå.

Ställ in macOS-baslinjen innan du lägger till scenerna

Använd ett SwiftUI-approjekt för macOS med distributionsmål macOS 14 eller senare och en Xcode-verktygskedja som stöder Observation, till exempel Xcode 15 eller senare. SwiftUI:s Observation-integrering börjar med macOS 14. MenuBarExtra och dess fönsterstil är tillgängliga från macOS 13, så Observation sätter den högre baslinjen för den här implementationen. Hantera modelldata i din app, MenuBarExtra, fönster.

Panelen i menyraden använder .menuBarExtraStyle(.window) eftersom den innehåller textfält och en anpassad layout. Apple beskriver den här stilen som ett popover-liknande fönster med kontroller som är utformade på liknande sätt som kontroller i ett vanligt fönster. Det är en separat scen från redigerarens WindowGroup. fönster.

Exemplet begränsar också uttryckligen sin modell och sina vytyper till @MainActor. Apple definierar huvudaktören som att den har en exekverare som motsvarar huvudkön för dispatch. Här kommer alla ändringar från UI-kontroller; inget bakgrundsarbete behövs. MainActor.

Bygg den gemensamma modellen och fristående fönsterutkast

Ersätt den genererade appens startpunkt med följande kompletta exempel i en enda fil. Behåll bara en deklaration av @main i målet. Exemplet innehåller importer, modell, vyer och starttext. Det har kontrollerats mot de dokumenterade API:erna, men kunde inte kompileras i den här miljön eftersom en Swift-verktygskedja saknades.

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

Det finns två uttryckliga injektioner i miljön: en för fönstrets innehåll och en för menyradens innehåll. Båda får store. Räkna inte med att en miljömodifierare på ett fönsters innehåll tillhandahåller ett beroende till en syskonscen. Apple dokumenterar delning av miljö genom en vyhierarki; de två injektionerna gör beroendet i varje hierarki uttryckligt. Hantera modelldata i din app.

Använd @Bindable när en kontroll behöver en bindning

SharedLabelControls läser modellen via @Environment(LabelStore.self). Inne i dess body tillhandahåller den lokala variabeln @Bindable den projicerade bindning som TextField behöver. Den hänvisar till det befintliga observerbara objektet och skapar inget annat datalager. Apple dokumenterar exakt det här mönstret från miljö till bindning. Bindable.

Utkastfältet använder i stället $draft, eftersom värdet redan ägs av fönstervyns @State. Knappen kopierar det aktuella utkastet till den gemensamma etiketten. Det är en uttrycklig åtgärd: senare ändringar av den gemensamma etiketten skriver inte om något av fönstrens utkast. Det här är redigeringens semantik i exemplet.

Matcha fönstergruppens identifierare exakt

Knappen i menyraden hämtar openWindow från sin egen miljö och anropar den med \"label-editor\", vilket matchar gruppdeklarationen. Apple dokumenterar den här identifierarbaserade metoden för att skapa ett fönster från en grupp. Knappens syfte är att öppna ytterligare en redigerare, så exemplet lovar inte att hitta och återanvända ett specifikt befintligt fönster. WindowGroup.

Kontrollera avgränsningen med en sekvens med två fönster

Följande sekvens kontrollerar det förväntade beteendet i exemplet. Den skiljer mellan en gemensam modellidentitet och fristående utkastlagring utan ytterligare instrumentering.

  1. Starta appen. Den första redigeraren visar den gemensamma etiketten Sample Alpha och utkastet Sample Draft.
  2. Öppna panelen i menyraden och välj Open Another Editor.
  3. Ändra utkastet i det första fönstret till Draft One. Låt utkastet i det andra fönstret vara oförändrat.
  4. Ändra den gemensamma etiketten till Sample Beta i panelen i menyraden. Båda redigeringsfönstren ska visa den gemensamma etiketten samtidigt som de behåller sina olika utkastvärden.
  5. Välj Use Draft as Shared Label i det första fönstret. Den gemensamma etiketten blir Draft One i båda fönstren och i panelen i menyraden. Utkastet i det andra fönstret är fortfarande Sample Draft.

Den här sekvensen kontrollerar tre skilda flöden: lokal inmatning, en ändring i menyraden och en uttrycklig kopiering från lokalt till gemensamt. Om lokal inmatning ändrar utkastet i det andra fönstret bör du kontrollera om utkastet har flyttats till appens datalager. Om ändringar från menyraden inte når fönstren bör du kontrollera om en ytterligare LabelStore() har skapats i menyradshierarkin.

Symptom Det första du bör kontrollera
Etiketterna skiljer sig mellan fönstret och menyraden Separata modellinitieringar i stället för en gemensam instans
Alla utkast ändras tillsammans Utkastlagring placerad i en gemensam modell
En vy misslyckas när den läser sin modell .environment(store) saknas i hierarkin
En redigerare återställs oväntat Ändringar i den ägande vyns identitet eller ersättning av dess modell
Det går inte att öppna ytterligare en redigerare Den deklarerade scenens identifierare och den begärda identifieraren stämmer inte överens

Fallet med en miljö som saknas har en dokumenterad konsekvens: när ett icke-valfritt observerbart objekt läses via typ förutsätter det att en förfader har tillhandahållit objektet; Apple säger att frånvaron av objektet orsakar ett undantag. Tillhandahåll också beroendet i förhandsvisningar och alternativa startpunkter. Hantera modelldata i din app.

Exemplet lagrar allt i minnet och har ingen implementering för att spara eller återställa data. Därför ska stängning av ett fönster betraktas som att dess ofärdiga utkast kastas bort, och avslut av appen som att den gemensamma etiketten kastas bort. Om utkasten måste finnas kvar efter dessa åtgärder bör du definiera ett separat krav på beständig lagring innan du utökar exemplet.

Håll fokuserade kommandon åtskilda från gemensamma kontroller i menyraden

En gemensam etikett kräver inget val av aktivt fönster: alla användare får samma modell. En åtgärd som redigerar utkastet i det fönster som har fokus har ett annat krav, eftersom den måste identifiera ett specifikt fönster.

För vanliga kommandon i appmenyn dokumenterar Apple hur man publicerar en fönsterägd modell med focusedSceneValue och använder den via @FocusedValue i en implementation av Commands. I exemplet inaktiveras kommandot när ingen modell har fokus. Det är en stödd metod för åtgärder vars mål följer den aktiva scenen. Bygga och anpassa menyraden med SwiftUI.

Dra inte slutsatsen från det kommandoexemplet att MenuBarExtra ger en tillförlitlig referens till den tidigare aktiva redigeraren. Panelen i den här artikeln arbetar medvetet med tillstånd som gäller hela appen. Om panelen måste rikta sig mot en enskild redigerare bör du lägga till en uttrycklig lösning för målval, till exempel en lista med appägda redigeraridentifierare, i stället för att anta vilket fönster som ska ta emot åtgärden.

Lägg till AppKit eller TextKit endast om redigeraren behöver det

Etikettredigeraren behöver bara SwiftUI-kontroller. Om kraven utökas till att omfatta en anpassad AppKit-textvy är NSViewRepresentable Apples stödda brygga för att skapa, uppdatera och avveckla en NSView i en SwiftUI-hierarki. Apple beskriver också hur en koordinator kan vidarebefordra meddelanden från delegerade metoder och målåtgärder till SwiftUI. NSViewRepresentable.

Behåll planen för ägarskap när du lägger till den bryggan: knyt varje redigeringsfönsters vyinstans och redigeringstillstånd till det fönstret, och skicka delat innehåll genom appmodellen endast där delning är avsedd. En koordinator bör vidarebefordra ändringar via en bindning eller modellåtgärd. Som en försiktighetsåtgärd vid implementationen bör du jämföra inkommande innehåll innan textvyinnehållet ersätts, så att orelaterade gemensamma uppdateringar inte återställer en pågående redigering upprepade gånger.

För en TextKit 2-implementation pekar Apple ut textLayoutManager som den moderna åtkomstpunkten till layoutmotorn. I Apples TextKit-session förklaras att åtkomst till det äldre layoutManager i en textvy med TextKit 2 kan utlösa kompatibilitetsläge, vilket växlar vyn till TextKit 1 utan automatisk återgång. Undvik att använda den äldre egenskapen enbart för att inspektera en textvy. TextKit, Nyheter i TextKit och textvyer.

Om macOS 13 måste stödjas behåller du samma ägarskapslösning men använder den äldre metoden för observation: ObservableObject med @Published, ett ägande @StateObject och .environmentObject tillsammans med mottagande egenskaper av typen @EnvironmentObject. Apples migreringsguide förklarar hur dessa mekanismer motsvarar det nyare Observation-sättet. Ett byte av observationssystem ska bevara de avsedda gränserna för delning. Migrera från Observable Object-protokollet till Observable-makrot.

Välj ägarskap utifrån hur någon förväntar sig att gränssnittet ska bete sig: en appägd instans för den gemensamma etiketten, ett fönsterägt värde för varje ofärdigt utkast och en uttrycklig åtgärd som kopplar ihop dem. Behåll sekvensen med två fönster som regressionskontroll när menyradspanelen eller redigeraren byggs ut.

Källor