Gestisci lo stato osservabile condiviso nella tua App SwiftUI, quindi passa la stessa istanza a entrambe le scene. Mantieni la bozza di ogni finestra nella relativa vista radice. In questo modo, un controllo nella barra dei menu può aggiornare tutte le finestre, mentre ciascuna conserva il proprio testo non ancora completato.

Questo tutorial realizza questa struttura per un piccolo editor di etichette per Mac: più finestre mostrano un’unica etichetta condivisa, ciascuna finestra ha una bozza indipendente e un pannello nella barra dei menu può modificare l’etichetta condivisa o aprire un’altra finestra. Tutti i testi di esempio appartengono all’app e usano etichette sintetiche.

Decidi quali modifiche devono raggiungere tutte le finestre

Inizia dal comportamento atteso per ciascun valore. Se l’etichetta condivisa viene modificata, deve cambiare ovunque. Una bozza non completata deve cambiare solo nella finestra in cui si scrive. Questi requisiti determinano dove collocare i valori.

Apple documenta che ogni finestra creata da un WindowGroup dispone di un’archiviazione indipendente per lo stato istanziato nella sua gerarchia di viste. Al contrario, le linee guida Apple sui dati del modello mostrano come gestire un riferimento osservabile nell’App di livello superiore e passarlo a una gerarchia di viste. La combinazione di questi approcci definisce il piano di gestione seguente. WindowGroup, Gestire i dati del modello nell’app.

Valore Responsabile in questo esempio Comportamento previsto
Etichetta condivisa App, con @State e un modello @Observable Tutte le finestre e il pannello della barra dei menu modificano lo stesso valore
Bozza non completata Vista radice di ogni finestra, con @State La digitazione resta confinata a quella finestra
Controlli della barra dei menu Ricevono il modello gestito dall’app Le modifiche raggiungono tutti i componenti che usano quell’istanza
Azione di apertura finestra Legge dall’ambiente della vista nella barra dei menu Apre una finestra dal gruppo identificato

La distinzione riguarda chi gestisce l’istanza. Creare due oggetti separati con lo stesso nome di classe non condivide i loro contenuti. Allo stesso modo, inserire un riferimento a un unico oggetto gestito dall’app in più finestre non crea copie indipendenti di quell’oggetto. Per questo l’esempio crea il modello condiviso una sola volta, a livello dell’app.

Definisci i requisiti di base di macOS prima di aggiungere le scene

Usa un progetto di app SwiftUI per macOS con una versione minima di macOS 14 o successiva e una toolchain di Xcode che supporti Observation, come Xcode 15 o successivo. L’integrazione di Observation in SwiftUI è disponibile da macOS 14. MenuBarExtra e il relativo stile finestra sono disponibili da macOS 13, quindi per questa implementazione Observation impone il requisito minimo più alto. Gestire i dati del modello nell’app, MenuBarExtra, window.

Il pannello della barra dei menu usa .menuBarExtraStyle(.window) perché contiene campi di testo e un layout personalizzato. Apple descrive questo stile come una finestra simile a un popover, con controlli dallo stile simile a quelli di una normale finestra. È una scena separata dal WindowGroup dell’editor. window.

L’esempio limita inoltre esplicitamente il modello e i tipi dell’interfaccia a @MainActor. Apple definisce l’attore principale come dotato di un esecutore equivalente alla coda di invio principale. In questo caso, tutte le modifiche provengono dai controlli dell’interfaccia e non è necessario alcun lavoro in background. MainActor.

Crea il modello condiviso e le bozze indipendenti delle finestre

Sostituisci il punto di ingresso dell’app generato con il seguente esempio completo in un unico file. Mantieni una sola dichiarazione @main nel target. L’esempio include importazioni, modello, viste e testo iniziale. È stato verificato rispetto alle API documentate, ma non è stato possibile compilarlo in questo ambiente perché non era disponibile una toolchain Swift.

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

Sono presenti due inserimenti espliciti nell’ambiente: uno per il contenuto della finestra e uno per il contenuto della barra dei menu. Entrambi ricevono store. Non aspettarti che un modificatore dell’ambiente applicato al contenuto di una finestra fornisca una dipendenza a una scena sorella. Apple documenta la condivisione dell’ambiente all’interno di una gerarchia di viste; i due inserimenti rendono esplicita la dipendenza di ciascuna gerarchia. Gestire i dati del modello nell’app.

Usa @Bindable quando un controllo ha bisogno di un binding

SharedLabelControls legge il modello tramite @Environment(LabelStore.self). Nel suo body, la variabile locale @Bindable fornisce il binding proiettato richiesto da TextField. Fa riferimento all’oggetto osservabile esistente e non crea un altro archivio. Apple documenta proprio questo schema, dall’ambiente al binding. Bindable.

Il campo della bozza usa invece $draft, perché il suo valore è già gestito da @State nella vista della finestra. Il pulsante copia la bozza corrente nell’etichetta condivisa. È un’azione esplicita: le modifiche successive all’etichetta condivisa non riscrivono la bozza di nessuna delle due finestre. Questa è la semantica di modifica scelta per l’esempio.

Fai corrispondere esattamente l’identificatore del gruppo di finestre

Il pulsante nella barra dei menu ottiene openWindow dal proprio ambiente e lo chiama con \"label-editor\", che corrisponde alla dichiarazione del gruppo. Apple documenta questo approccio basato su un identificatore per creare una finestra da un gruppo. Lo scopo del pulsante è aprire un altro editor, quindi questo esempio non garantisce di individuare e riutilizzare una specifica finestra esistente. WindowGroup.

Verifica l’ambito con una sequenza a due finestre

La sequenza seguente verifica il comportamento previsto per l’esempio. Permette di distinguere l’identità del modello condiviso dall’archiviazione indipendente delle bozze senza richiedere strumenti aggiuntivi.

  1. Avvia l’app. Il primo editor mostra l’etichetta condivisa Sample Alpha e la bozza Sample Draft.
  2. Apri il pannello della barra dei menu e scegli Open Another Editor.
  3. Modifica la bozza della prima finestra in Draft One. Lascia invariata quella della seconda finestra.
  4. Nel pannello della barra dei menu, modifica l’etichetta condivisa in Sample Beta. Entrambe le finestre dell’editor dovrebbero mostrare l’etichetta condivisa, conservando le rispettive bozze diverse.
  5. Nella prima finestra, scegli Use Draft as Shared Label. L’etichetta condivisa diventa Draft One in entrambe le finestre e nel pannello della barra dei menu. La bozza della seconda finestra resta Sample Draft.

Questa sequenza verifica tre percorsi distinti: digitazione locale, modifica dalla barra dei menu e copia esplicita da locale a condiviso. Se la digitazione locale modifica la bozza dell’altra finestra, controlla se la bozza è stata spostata nell’archivio gestito dall’app. Se le modifiche dalla barra dei menu non raggiungono le finestre, controlla se nella gerarchia della barra dei menu è stato creato un altro LabelStore().

Sintomo Primo controllo da fare
Le etichette nella finestra e nella barra dei menu divergono Inizializzazioni separate del modello invece di una sola istanza condivisa
Tutte le bozze cambiano insieme Bozze collocate in un modello condiviso
Una vista genera un errore quando legge il proprio modello In quella gerarchia manca .environment(store)
Un editor si azzera inaspettatamente Modifiche all’identità della vista responsabile o sostituzione del suo modello
Non si riesce ad aprire un altro editor Incongruenza tra l’identificatore della scena dichiarato e quello richiesto

Nel caso in cui l’ambiente sia assente, le conseguenze sono documentate: la lettura di un oggetto osservabile non opzionale per tipo presuppone che un antenato lo abbia fornito; Apple afferma che l’assenza dell’oggetto causa un’eccezione. Fornisci la dipendenza anche nelle anteprime e nei punti di ingresso alternativi. Gestire i dati del modello nell’app.

L’esempio archivia tutto in memoria e non implementa alcun salvataggio o ripristino. Pertanto, la chiusura di una finestra va considerata come l’eliminazione della relativa bozza non completata, mentre l’uscita dall’app va considerata come l’eliminazione dell’etichetta condivisa. Se le bozze devono sopravvivere a queste azioni, definisci un requisito di persistenza separato prima di estendere questo esempio.

Mantieni separati i comandi contestuali e i controlli condivisi della barra dei menu

L’etichetta condivisa non richiede di selezionare la finestra attiva: tutti i componenti ricevono lo stesso modello. Un’azione che modifica la bozza della finestra attualmente in primo piano ha un requisito diverso, perché deve identificare una finestra specifica.

Per i comandi standard del menu dell’applicazione, Apple documenta la pubblicazione di un modello gestito dalla finestra con focusedSceneValue e il suo utilizzo tramite @FocusedValue in un’implementazione di Commands. Nell’esempio Apple, il comando è disabilitato quando non esiste alcun modello attivo. È un approccio supportato per le azioni il cui destinatario segue la scena attiva. Creare e personalizzare la barra dei menu con SwiftUI.

Non dedurre da quell’esempio di comando che l’apertura di un MenuBarExtra gli fornisca un riferimento affidabile all’editor attivo in precedenza. Il pannello di questo articolo opera intenzionalmente sullo stato dell’intera app. Se il pannello deve agire su un editor specifico, aggiungi un sistema esplicito di selezione del destinatario, ad esempio un elenco di identificatori degli editor gestiti dall’app, invece di presumere quale finestra debba ricevere l’azione.

Aggiungi AppKit o TextKit solo se l’editor ne ha bisogno

L’editor di etichette richiede solo controlli SwiftUI. Se i requisiti arrivano a includere una vista di testo AppKit personalizzata, NSViewRepresentable è il bridge supportato da Apple per creare, aggiornare e smantellare una NSView in una gerarchia SwiftUI. Apple descrive anche come usare un coordinatore per inoltrare a SwiftUI i messaggi di delegati e target-action. NSViewRepresentable.

Quando aggiungi quel bridge, mantieni il piano di gestione: associa l’istanza della vista e lo stato di modifica di ogni editor alla rispettiva finestra e passa i contenuti condivisi tramite il modello dell’app solo dove la condivisione è prevista. Un coordinatore dovrebbe inoltrare le modifiche tramite un binding o un’azione del modello. Come precauzione d’implementazione, confronta i contenuti in ingresso prima di sostituire quelli della vista di testo, così gli aggiornamenti condivisi non correlati non azzerano ripetutamente una modifica in corso.

Per un’implementazione con TextKit 2, Apple indica textLayoutManager come punto di accesso moderno al motore di layout. La sessione di Apple su TextKit spiega che l’accesso al vecchio layoutManager in una vista di testo TextKit 2 può attivare la modalità di compatibilità e passare quella vista a TextKit 1 senza ritorno automatico. Evita di usare quella proprietà obsoleta solo per esaminare una vista di testo. TextKit, Novità in TextKit e nelle viste di testo.

Se è necessario supportare macOS 13, mantieni la stessa struttura di gestione, ma usa il percorso di osservazione precedente: ObservableObject con @Published, un @StateObject responsabile e .environmentObject con proprietà riceventi @EnvironmentObject. La guida alla migrazione di Apple spiega come questi meccanismi corrispondono al nuovo approccio Observation. Il passaggio tra sistemi di osservazione deve mantenere i confini di condivisione previsti. Migrazione dal protocollo Observable Object alla macro Observable.

Scegli chi gestisce i valori in base al comportamento atteso dell’interfaccia: un’unica istanza gestita dall’app per l’etichetta condivisa, un valore gestito da ciascuna finestra per ogni bozza non completata e un’azione esplicita che li collega. Mantieni la sequenza a due finestre come controllo di regressione ogni volta che il pannello della barra dei menu o l’editor si amplia.

Fonti