Mantenha o estado observável compartilhado no seu App do SwiftUI e passe a mesma instância para ambas as cenas. Mantenha o rascunho de cada janela dentro da sua view raiz. Assim, um controle da barra de menus pode atualizar todas as janelas, enquanto cada uma mantém seu próprio texto inacabado.
Este tutorial cria essa estrutura para um pequeno editor de rótulos para Mac: várias janelas mostram um rótulo compartilhado, cada janela tem um rascunho independente e um painel da barra de menus pode editar o rótulo compartilhado ou abrir outra janela. Todo o texto de exemplo pertence ao app e usa rótulos fictícios.
Decida quais alterações devem chegar a todas as janelas
Comece pelo comportamento esperado de cada valor. Um rótulo compartilhado deve mudar em todos os lugares quando for editado. Um rascunho inacabado deve mudar somente na janela em que alguém digita. Esses requisitos determinam onde os valores devem ficar.
A Apple documenta que cada janela criada por um WindowGroup tem armazenamento independente para o estado instanciado dentro da hierarquia de views. Em contrapartida, as orientações da Apple sobre dados de modelo demonstram como manter uma referência observável no App de nível superior e passá-la para uma hierarquia de views. A combinação desses padrões resulta neste plano de propriedade. WindowGroup, Gerenciamento dos dados de modelo no app.
| Valor | Responsável neste exemplo | Comportamento pretendido |
|---|---|---|
| Rótulo compartilhado | App, usando @State com um modelo @Observable |
Todas as janelas e o painel da barra de menus editam o mesmo valor |
| Rascunho inacabado | View raiz de cada janela, usando @State |
A digitação fica restrita àquela janela |
| Controles da barra de menus | Recebem o modelo pertencente ao app | As alterações chegam a todos os consumidores dessa instância |
| Ação para abrir janela | Lida do ambiente da view da barra de menus | Abre uma janela do grupo identificado |
A distinção está em quem é responsável pela instância. Dar o mesmo nome de classe a dois objetos criados separadamente não compartilha o conteúdo deles. Da mesma forma, colocar uma referência a um único objeto pertencente ao app em várias janelas não cria cópias independentes desse objeto. Por isso, o exemplo cria seu modelo compartilhado uma única vez, no nível do app.
Defina a versão mínima do macOS antes de adicionar as cenas
Use um projeto de app SwiftUI para macOS com uma versão mínima de implantação do macOS 14 ou posterior e uma cadeia de ferramentas do Xcode compatível com Observation, como o Xcode 15 ou posterior. A integração do Observation com o SwiftUI começa no macOS 14. MenuBarExtra e seu estilo de janela estão disponíveis desde o macOS 13; portanto, o Observation define a versão mínima mais alta desta implementação. Gerenciamento dos dados de modelo no app, MenuBarExtra, janela.
O painel da barra de menus usa .menuBarExtraStyle(.window) porque contém campos de texto e um layout personalizado. A Apple descreve esse estilo como uma janela semelhante a um popover, com controles estilizados de forma semelhante aos de uma janela normal. É uma cena separada do WindowGroup do editor. janela.
O exemplo também isola explicitamente o modelo e os tipos de interface em @MainActor. A Apple define o ator principal como tendo um executor equivalente à fila principal de despacho. Neste caso, todas as mutações se originam de controles da interface; não é necessário executar trabalho em segundo plano. MainActor.
Crie o modelo compartilhado e rascunhos independentes para as janelas
Substitua o ponto de entrada gerado do app pelo exemplo completo de arquivo único a seguir. Mantenha apenas uma declaração @main no destino. O exemplo inclui as importações, o modelo, as views e o texto inicial. Ele foi conferido com base nas APIs documentadas, mas não foi possível compilá-lo neste ambiente porque não havia uma cadeia de ferramentas Swift disponível.
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)
}
}
Há duas injeções explícitas no ambiente: uma no conteúdo da janela e outra no conteúdo da barra de menus. Ambas recebem store. Não espere que um modificador de ambiente aplicado ao conteúdo de uma janela forneça uma dependência a uma cena irmã. A Apple documenta o compartilhamento de ambiente por uma hierarquia de views; as duas injeções deixam explícita a dependência de cada hierarquia. Gerenciamento dos dados de modelo no app.
Use @Bindable quando um controle precisar de um binding
SharedLabelControls lê o modelo por meio de @Environment(LabelStore.self). Dentro do corpo da view, a variável local @Bindable fornece o binding projetado exigido por TextField. Ela se refere ao objeto observável existente e não cria outro armazenamento. A Apple documenta exatamente esse padrão de ambiente para binding. Bindable.
O campo de rascunho usa $draft, pois seu valor já pertence ao @State da view da janela. O botão copia o rascunho atual para o rótulo compartilhado. Essa é uma ação explícita: alterações posteriores no rótulo compartilhado não reescrevem o rascunho de nenhuma das janelas. Essa é a semântica de edição escolhida para o exemplo.
Faça o identificador do grupo de janelas corresponder exatamente
O botão da barra de menus obtém openWindow do próprio ambiente e o chama com \"label-editor\", que corresponde à declaração do grupo. A Apple documenta essa abordagem baseada em identificador para criar uma janela de um grupo. O objetivo do botão é abrir outro editor; portanto, este exemplo não promete localizar e reutilizar uma janela existente específica. WindowGroup.
Verifique o escopo com uma sequência de duas janelas
A sequência a seguir verifica o comportamento esperado do exemplo. Ela diferencia a identidade do modelo compartilhado do armazenamento independente dos rascunhos sem exigir instrumentação adicional.
- Inicie o app. O primeiro editor mostra o rótulo compartilhado
Sample Alphae o rascunhoSample Draft. - Abra o painel da barra de menus e escolha
Open Another Editor. - Altere o rascunho da primeira janela para
Draft One. Deixe o rascunho da segunda janela inalterado. - No painel da barra de menus, altere o rótulo compartilhado para
Sample Beta. As duas janelas do editor devem exibir esse rótulo compartilhado, mantendo seus diferentes valores de rascunho. - Na primeira janela, escolha
Use Draft as Shared Label. O rótulo compartilhado passa a serDraft Onenas duas janelas e no painel da barra de menus. O rascunho da segunda janela permanece comoSample Draft.
Essa sequência verifica três caminhos distintos: digitação local, uma edição pela barra de menus e uma cópia explícita do valor local para o compartilhado. Se a digitação local alterar o rascunho da outra janela, verifique se o rascunho foi transferido para o armazenamento pertencente ao app. Se as edições na barra de menus não chegarem às janelas, verifique se outro LabelStore() foi criado na hierarquia da barra de menus.
| Sintoma | Primeira coisa a verificar |
|---|---|
| Os rótulos da janela e da barra de menus divergem | Inicializações de modelos separadas em vez de uma única instância compartilhada |
| Todos os rascunhos mudam juntos | Armazenamento dos rascunhos colocado em um modelo compartilhado |
| Uma view falha ao ler seu modelo | Falta de .environment(store) naquela hierarquia |
| Um editor é redefinido inesperadamente | Alterações na identidade da view responsável ou substituição do modelo dela |
| Não é possível abrir outro editor | Incompatibilidade entre o identificador de cena declarado e o solicitado |
O caso de ambiente ausente tem uma consequência documentada: ler um objeto observável não opcional por tipo pressupõe que um ancestral o forneceu; a Apple afirma que a ausência do objeto causa uma exceção. Forneça essa dependência também nas pré-visualizações e nos pontos de entrada alternativos. Gerenciamento dos dados de modelo no app.
O exemplo armazena tudo na memória e não implementa salvamento nem restauração. Portanto, fechar uma janela deve ser entendido como descartar o rascunho inacabado, e sair do app, como descartar o rótulo compartilhado. Se os rascunhos precisarem sobreviver a essas ações, defina um requisito de persistência separado antes de estender este exemplo.
Mantenha os comandos com foco separados dos controles compartilhados da barra de menus
Um rótulo compartilhado não precisa selecionar uma janela ativa: todos os consumidores recebem o mesmo modelo. Uma ação que edita o rascunho da janela que está em foco tem um requisito diferente, pois precisa identificar uma janela específica.
Para comandos de menu padrão do app, a Apple documenta como publicar um modelo pertencente à janela com focusedSceneValue e consumi-lo por meio de @FocusedValue em uma implementação de Commands. O exemplo desativa o comando quando não há nenhum modelo em foco. Esse é um caminho compatível para ações cujo destino acompanha a cena ativa. Criar e personalizar a barra de menus com o SwiftUI.
Não conclua, a partir desse exemplo de comando, que abrir um MenuBarExtra lhe dá uma referência confiável ao editor que estava ativo anteriormente. Este artigo faz com que o painel opere deliberadamente sobre o estado de todo o app. Se o painel precisar ter como destino um editor individual, adicione um mecanismo explícito para selecionar o destino, como uma lista de identificadores de editores pertencentes ao app, em vez de presumir qual janela deve receber a ação.
Adicione AppKit ou TextKit somente se o editor precisar deles
O editor de rótulos precisa apenas de controles do SwiftUI. Se os requisitos passarem a incluir uma view de texto personalizada do AppKit, NSViewRepresentable será a ponte compatível da Apple para criar, atualizar e desmontar uma NSView em uma hierarquia do SwiftUI. A Apple também descreve o uso de um coordenador para encaminhar mensagens de delegado e de target-action ao SwiftUI. NSViewRepresentable.
Preserve o plano de propriedade ao adicionar essa ponte: mantenha a instância da view e o estado de edição de cada editor associados à janela e passe o conteúdo compartilhado pelo modelo do app somente onde o compartilhamento for intencional. Um coordenador deve encaminhar as edições por meio de um binding ou de uma ação do modelo. Como precaução de implementação, compare o conteúdo recebido antes de substituir o conteúdo da view de texto, para que atualizações compartilhadas sem relação com a edição não redefinam repetidamente uma edição em andamento.
Para uma implementação com TextKit 2, a Apple identifica textLayoutManager como o ponto de acesso moderno ao mecanismo de layout. A sessão da Apple sobre TextKit explica que acessar o layoutManager legado em uma view de texto do TextKit 2 pode ativar o modo de compatibilidade, mudando essa view para o TextKit 1 sem retornar automaticamente. Evite usar essa propriedade legada apenas para inspecionar uma view de texto. TextKit, Novidades no TextKit e nas views de texto.
Se for necessário oferecer suporte ao macOS 13, mantenha a mesma estrutura de propriedade, mas use o caminho de observação mais antigo: ObservableObject com @Published, um @StateObject responsável e .environmentObject com propriedades receptoras @EnvironmentObject. O guia de migração da Apple explica como esses mecanismos correspondem à abordagem mais recente do Observation. A mudança do sistema de observação deve preservar os limites de compartilhamento pretendidos. Migração do protocolo Observable Object para a macro Observable.
Escolha a propriedade de acordo com o comportamento esperado da interface: uma instância pertencente ao app para o rótulo compartilhado, um valor pertencente a cada janela para cada rascunho inacabado e uma ação explícita que os conecte. Mantenha a sequência de duas janelas como verificação de regressão sempre que o painel da barra de menus ou o editor crescer.
Fontes
- Gerenciamento dos dados de modelo no app — Apple
- WindowGroup — Apple
- MenuBarExtra — Apple
- janela — Apple
- MainActor — Apple
- Bindable — Apple
- Criar e personalizar a barra de menus com o SwiftUI — Apple
- NSViewRepresentable — Apple
- TextKit — Apple
- Novidades no TextKit e nas views de texto — Apple
- Migração do protocolo Observable Object para a macro Observable — Apple