Use sheet(item:onDismiss:content:) com um único item selecionado opcional e construa a sheet a partir do item recebido pela closure. Assim, um app para Mac tem um único valor que determina tanto se uma sheet será apresentada quanto o que ela exibirá.
Este tutorial implementa uma pequena tarefa para desktop: clicar em um item de exemplo local e inspecionar seus detalhes imediatamente. Ele também apresenta uma sequência de diagnóstico para uma sheet que abre em branco, mostra a seleção anterior ou exibe texto desatualizado dentro de uma view do AppKit.
Por que usar uma sheet baseada em item?
O SwiftUI oferece dois métodos de apresentação relevantes. A sobrecarga com Boolean apresenta a sheet quando o binding passa a ser verdadeiro. A sobrecarga com item aceita um valor identificável opcional e fornece esse valor à closure de conteúdo. Escolha a forma com Boolean para um painel fixo; escolha a forma com item quando o conteúdo do painel depender de uma seleção. Consulte a documentação da Apple sobre sheets baseadas em Boolean e sheets baseadas em item.
Um design com propriedades separadas para seleção e apresentação permite combinações como uma apresentação solicitada sem nenhuma seleção. Ao combiná-las em um único valor opcional, essa combinação deixa de fazer parte do seu próprio modelo de estado: nil representa que nenhum item foi solicitado, e um valor preenchido representa o item a inspecionar. Essa é uma dedução de design baseada nas duas APIs, não uma afirmação sobre um defeito de agendamento não documentado do SwiftUI.
Se uma implementação existente atribui uma seleção e depois ativa um Boolean, essas duas instruções, por si só, não demonstram por que a primeira apresentação falha. Inspecione o fluxo de dados do conteúdo antes de atribuir o sintoma ao timing. A pergunta útil é se a sheet renderiza a entrada fornecida, lê outra propriedade ou exibe uma cópia armazenada separadamente.
Crie um exemplo completo para macOS
Crie no Xcode um projeto de app para macOS com a interface SwiftUI. Use macOS 12 ou posterior como destino de implantação para este exemplo. A API de sheet baseada em item está disponível desde o macOS 10.15, enquanto a ação de dismiss do ambiente usada abaixo requer macOS 12. A Apple documenta esses limites de disponibilidade para sheets baseadas em item e o valor de ambiente de dismiss.
Substitua a declaração do app e a view de conteúdo geradas por este programa completo. Mantenha apenas um ponto de entrada do app no destino. Todos os rótulos e descrições abaixo são dados de exemplo locais e fictícios.
import SwiftUI
struct SampleItem: Identifiable {
let id: String
let title: String
let detail: String
}
@main
struct FirstSheetDemoApp: App {
var body: some Scene {
WindowGroup {
ItemListView()
}
}
}
struct ItemListView: View {
private let items = [
SampleItem(
id: "tile-a",
title: "Amber Tile",
detail: "A square sample with an amber finish."
),
SampleItem(
id: "tile-b",
title: "Blue Tile",
detail: "A rectangular sample with a blue finish."
)
]
@State private var presentedItem: SampleItem? = nil
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Sample Items")
.font(.title2)
ForEach(items) { item in
Button("Inspect \(item.title)") {
presentedItem = item
}
}
}
.padding(24)
.frame(minWidth: 360, minHeight: 200)
.sheet(item: $presentedItem) { item in
ItemDetailView(item: item)
}
}
}
struct ItemDetailView: View {
let item: SampleItem
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(item.title)
.font(.title)
Text(item.detail)
Text("Item ID: \(item.id)")
.font(.caption)
HStack {
Spacer()
Button("Done") {
dismiss()
}
}
}
.padding(24)
.frame(width: 380, height: 220)
}
}
O botão atribui o valor clicado a presentedItem. Em seguida, a closure de apresentação passa seu próprio parâmetro item para ItemDetailView. Observe que a view de detalhes tem uma propriedade de entrada simples, let item, em vez de uma segunda propriedade de estado para o item selecionado. A Apple recomenda manter uma única fonte da verdade e compartilhar valores somente leitura por meio de propriedades, usando bindings quando uma view filha precisar de acesso para escrita. Consulte State.
A ação de dismiss pertence à ItemDetailView, onde ela lê o ambiente da sheet. A Apple alerta explicitamente que, se você a ler na view que apresenta a sheet e invocar essa ação no conteúdo da sheet, será usado o ambiente da view apresentadora. Mantenha a declaração junto ao próprio conteúdo da sheet, como mostrado. Consulte dismiss.
O programa foi inspecionado para verificar a consistência das entradas e referências; ele não foi compilado nem executado aqui porque este ambiente não tem uma toolchain do Swift disponível. Execute as verificações a seguir no Xcode, na versão de macOS de destino.
Verifique a primeira apresentação com uma sequência deliberada de cliques
Comece cada verificação da primeira apresentação com uma inicialização nova. Testar somente depois de abrir outro item pode ocultar o sintoma original. As expectativas a seguir decorrem dos rótulos e das atribuições de dados explícitos do exemplo; são critérios de aceitação, não resultados de testes relatados.
| Ação | Conteúdo esperado da sheet | O que verifica |
|---|---|---|
Inicie o app e clique primeiro em Inspect Blue Tile |
Blue Tile, sua descrição retangular e tile-b |
A primeira apresentação usa o item clicado |
Clique em Done e depois inspecione Amber Tile |
Amber Tile, sua descrição quadrada e tile-a |
Uma seleção posterior chega ao conteúdo |
Dispense a sheet e inspecione Amber Tile novamente |
Os mesmos detalhes de amber | A reabertura do item continua consistente |
Reinicie o app e inspecione Amber Tile primeiro |
Os detalhes de amber imediatamente | O resultado correto independe de qual exemplo é aberto primeiro |
Mostrar um título e um ID torna essa verificação mais útil do que conferir apenas o título. Se sua interface real permitir nomes de exibição duplicados, use durante o desenvolvimento dois itens fictícios com o mesmo título e IDs e descrições diferentes. Isso revela seleções acidentais baseadas no texto exibido.
Diagnostique conteúdo desatualizado, do botão à view final
Siga estas verificações na ordem apresentada. Cada etapa restringe o local da divergência, para que você possa alterar a camada responsável sem adicionar soluções alternativas de apresentação sem relação com o problema.
- Verifique a entrada da ação. Defina um breakpoint na ação do botão clicado e inspecione
item.id. Confirme que a ação recebe o registro pretendido. - Verifique a entrada da apresentação. Inspecione o parâmetro
itemdentro da closure da sheet. Passe esse parâmetro para a view de detalhes, em vez de buscar novamente uma seleção separada. - Verifique a propriedade exibida por último. Confirme que o título e a descrição vêm da entrada da view de detalhes. Procure uma propriedade em cache ou um rascunho local que contenha um valor anterior.
- Verifique qualquer ponte para o AppKit. Se o texto comum do SwiftUI estiver correto, mas o texto encapsulado estiver desatualizado, inspecione o método de atualização do representable.
Mantenha a identidade estável
A API baseada em item exige Identifiable. O Swift define esse protocolo em torno de uma identidade estável e deixa sua duração e seu escopo a cargo do tipo que o adota e de quem o consome. O exemplo usa IDs de string armazenados que distinguem os dois registros. Consulte Identifiable.
Para uma lista real, escolha um identificador que continue identificando o mesmo registro durante as atualizações aceitas pela sua interface. Evite derivar a identidade de um título que possa mudar ou ser compartilhado por vários itens. Evite também um id calculado que crie um novo UUID toda vez que for lido; isso não forneceria uma identidade estável.
Separe a entrada de exibição de um rascunho editável
Um inspetor pode exibir item.title diretamente. Um editor pode criar intencionalmente um rascunho local, mas um rascunho introduz outro valor cujo ciclo de vida precisa ser definido. Decida quando ele será criado, se alterações posteriores no modelo devem substituí-lo e o que acontece quando o usuário termina ou cancela.
A Apple explica que o SwiftUI preserva o armazenamento de estado para a identidade de uma view e o inicializa no começo do ciclo de vida dessa identidade. Recriar um valor de view não cria, por si só, um estado novo. Por isso, inicializar um estado local a partir de uma entrada não é um mecanismo geral para manter esse estado sincronizado com entradas posteriores. Consulte Demystify SwiftUI.
Para esta tarefa somente de leitura, mantenha a propriedade de entrada simples. Se você estender o exemplo para permitir edição, escolha um design explícito de rascunho e confirmação ou um binding para o valor pretendido do modelo. Evite adicionar uma identidade de view variável apenas para atualizar o texto: mudar a identidade substitui o estado associado, o que pode descartar um rascunho inacabado. Essa consequência decorre do modelo de ciclo de vida de estado documentado pela Apple.
O que muda quando a sheet contém uma view de texto do AppKit?
Uma sheet que contém um NSTextView ainda precisa do mesmo fluxo de dados do item selecionado. Ela também precisa de uma ponte implementada corretamente. O protocolo NSViewRepresentable da Apple separa a criação inicial em makeNSView(context:) da configuração posterior em updateNSView(_:context:). Um wrapper que atribui seu texto somente durante a criação não tem uma implementação para aplicar entradas de texto posteriores. Consulte NSViewRepresentable.
Para um wrapper somente de leitura, inicialize a view nativa com o texto fornecido e faça com que seu método de atualização sincronize o texto exibido com a entrada atual quando necessário. A Apple documenta que o SwiftUI chama o método de atualização quando há alterações que afetam a view correspondente do AppKit. Consulte updateNSView(_:context:).
Um wrapper editável precisa de uma política adicional: não substitua cegamente seu texto em toda atualização, sobrescrevendo edições em andamento. Use um coordenador para comunicar as alterações feitas por delegados ou ações de destino nativas de volta ao SwiftUI, conforme descrito pela Apple na documentação do representable. Deixe o SwiftUI controlar o frame e os limites da view representada; a Apple alerta contra definir essas propriedades de layout diretamente.
O TextKit trata do armazenamento, layout e renderização de texto. A Apple documenta tanto o caminho moderno do NSTextLayoutManager quanto o caminho legado do NSLayoutManager, disponíveis por meio de NSTextView. Esses recursos não fornecem o registro selecionado pela sheet. Confirme que a ponte recebe o texto do item correto antes de investigar o layout do texto e verifique os requisitos de disponibilidade separadamente antes de introduzir APIs adicionais do TextKit. Consulte TextKit.
Escolha o modelo de apresentação adequado à tarefa
Use a sobrecarga com Boolean quando o conteúdo for fixo e a visibilidade for a única entrada que muda. Para esta tarefa de inspetor, use a sobrecarga com item. A Apple documenta que mudar o item fornecido pode dispensar e substituir uma sheet existente; portanto, não trate a substituição do item como uma garantia de edição ininterrupta durante a mesma apresentação. Consulte sheet(item:onDismiss:content:).
Para garantir a exibição correta desde a primeira apresentação, facilite o rastreamento do registro selecionado: uma atribuição na ação do botão, um parâmetro de item na closure da sheet e uma entrada na view de detalhes. Verifique os dois itens de exemplo depois de iniciar o app do zero. Se o resultado ainda divergir, a sequência de diagnóstico identifica se a divergência vem da seleção, do estado preservado ou do caminho de atualização da view nativa.
Fontes
- sheet(item:onDismiss:content:) — Apple
- sheet(isPresented:onDismiss:content:) — Apple
- State — Apple
- dismiss — Apple
- Identifiable — Apple
- Demystify SwiftUI — Apple
- NSViewRepresentable — Apple
- updateNSView(_:context:) — Apple
- TextKit — Apple