Torne seu componente SwiftUI genérico sobre o tipo que está em conformidade com o protocolo e, em seguida, declare a visualização armazenada como Provider.Content. Isso preserva a relação entre o provedor e seu tipo de visualização associado em todo o componente.
Este tutorial cria uma pequena janela do macOS com dois blocos sintéticos. O objetivo é armazenar um provedor e a visualização que ele produz sem substituir o tipo associado por any View ou AnyView. O mesmo padrão se aplica quando um componente de desktop reutilizável aceita um rótulo personalizado, um acessório ou um provedor de conteúdo.
Que relação as propriedades armazenadas precisam preservar?
Um tipo associado nomeia um tipo escolhido por cada conformidade com o protocolo. Se um protocolo declara associatedtype Content: View, um provedor que esteja em conformidade precisa fornecer um tipo de conteúdo específico. O código genérico pode se referir a esse tipo como Provider.Content, mesmo quando sua implementação está oculta por trás de um resultado opaco. A documentação do Swift aborda tipos associados e restrições genéricas no capítulo sobre Generics.
Portanto, o componente precisa de duas declarações relacionadas: um parâmetro genérico restrito ao protocolo do provedor e uma propriedade de conteúdo armazenada cujo tipo seja o tipo associado desse parâmetro. Declarar duas propriedades independentes que apenas estejam em conformidade com protocolos não expressa essa relação.
O próprio SwiftUI usa um contrato relacionado: View declara associatedtype Body: View, e o Swift infere esse tipo associado a partir da implementação de body de uma visualização personalizada. O Content do provedor e o Body do componente são tipos associados separados; eles não precisam ser iguais. Consulte a documentação de Body da Apple.
Crie um exemplo completo para macOS
Para este exemplo, use um projeto de app para macOS no Xcode com SwiftUI e destino de implantação no macOS 13 ou posterior. Substitua as declarações de app e de visualização de conteúdo geradas pelo seguinte arquivo único, mantendo exatamente uma declaração @main no destino. A Apple documenta essa estrutura de entrada do app e o uso de WindowGroup em App.
O exemplo usa deliberadamente a sintaxe tradicional de tipos associados e genéricos nomeados. Os resultados opacos foram implementados no Swift 5.1; a sintaxe opcional de tipos associados primários discutida mais adiante requer o Swift 5.7. O arquivo completo foi elaborado para uma versão atual do toolchain do Swift 6. Ele foi conferido com base na documentação consultada, mas não foi compilado aqui porque este ambiente não tem um compilador Swift. O destino de implantação do macOS escolhido é um pré-requisito do exemplo, não uma afirmação sobre a versão mais antiga do sistema operacional compatível com todos os recursos subjacentes. Consulte SE-0244 e SE-0346.
import SwiftUI
@MainActor
protocol TileProvider {
associatedtype Content: View
var title: String { get }
func makeContent() -> Content
}
@MainActor
struct TextTile: TileProvider {
let title: String
let message: String
func makeContent() -> Text {
Text(message)
}
}
@MainActor
struct CircleTile: TileProvider {
let title: String
let caption: String
func makeContent() -> some View {
HStack(spacing: 8) {
Circle()
.fill(Color.blue)
.frame(width: 16, height: 16)
Text(caption)
}
}
}
@MainActor
struct TileCard<Provider: TileProvider>: View {
private let provider: Provider
private let content: Provider.Content
init(provider: Provider) {
self.provider = provider
self.content = provider.makeContent()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(provider.title)
.font(.headline)
content
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.12))
}
}
@MainActor
struct ContentView: View {
var body: some View {
VStack(spacing: 12) {
TileCard(provider: TextTile(
title: "Text tile",
message: "Blue square"
))
TileCard(provider: CircleTile(
title: "Shape tile",
caption: "Blue circle"
))
}
.padding()
.frame(width: 360)
}
}
@main
@MainActor
struct AssociatedViewDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Compile e execute o destino para macOS. O resultado esperado é uma janela com um bloco de texto e outro com um círculo azul ao lado da legenda. Ambos usam o mesmo componente genérico, com o tipo do provedor inferido independentemente em cada ponto de chamada.
As anotações explícitas @MainActor mantêm essas declarações que produzem interface do usuário no mesmo contexto de ator. A documentação atual de View da Apple declara o protocolo com isolamento de ator principal. As anotações aqui também deixam explícito o contexto pretendido do protocolo personalizado do provedor.
Por que o componente genérico preserva o tipo?
No primeiro cartão, Provider é TextTile, então Provider.Content é Text. No segundo cartão, Provider é CircleTile, e seu tipo de conteúdo associado é o resultado opaco de CircleTile.makeContent(). Você pode nomear esse tipo por meio de CircleTile.Content sem explicitar o tipo aninhado de layout do SwiftUI.
A atribuição essencial é self.content = provider.makeContent(). O protocolo garante que esse método retorne exatamente Provider.Content, que é exatamente o tipo declarado da propriedade armazenada. Essa relação em tempo de compilação deriva da restrição genérica, em vez de uma comparação de tipos em tempo de execução. O capítulo sobre Generics do Swift explica como acessar tipos associados por meio de parâmetros genéricos restritos.
Usar some View na implementação em conformidade não apaga essa relação. Um resultado opaco oculta a implementação concreta, mas preserva a identidade do tipo. Isso o torna adequado como testemunha de tipo associado. O Swift explica a distinção em Tipos de protocolo opacos e encaixotados.
Esses cartões têm especializações diferentes: TileCard<TextTile> e TileCard<CircleTile>. Reutilizar a declaração de origem não exige que todos os cartões tenham um único tipo universal de conteúdo armazenado.
Por que propriedades opacas ou existenciais independentes não resolvem o problema?
Os trechos a seguir são exemplos para diagnóstico, não substituições do programa completo:
// Diagnostic fragment: no initializer establishes the opaque type.
let content: some View
// Diagnostic fragment: hides which conforming provider is stored.
let provider: any TileProvider
// Diagnostic fragment: stores an existential view value.
let content: any View
Uma propriedade armazenada opaca precisa de um inicializador na declaração que estabeleça seu tipo subjacente. Escrever some View não cria um parâmetro genérico escolhido pelo chamador que um inicializador de componente arbitrário possa preencher depois. Por exemplo, let content: some View = Text("Sample") estabelece um tipo para essa declaração; não conecta a propriedade ao tipo associado de outra propriedade. Essa regra para propriedades armazenadas está documentada em SE-0244: Tipos de resultado opacos.
Uma existencial como any TileProvider permite armazenar diferentes tipos que estejam em conformidade. Ela oculta o tipo específico em conformidade, então a declaração deixa de expor a relação como Provider.Content. Da mesma forma, any View é um contêiner existencial; não é o mesmo contrato que um tipo genérico nomeado em conformidade com View. Consulte a explicação do Swift sobre tipos de protocolo encaixotados.
O Swift pode abrir implicitamente argumentos existenciais em algumas chamadas genéricas. Isso não torna toda existencial utilizável em qualquer inicializador genérico. Em particular, a SE-0352 restringe a abertura quando o tipo subjacente desconhecido escaparia dentro de um resultado genérico, como X<T>. Uma especialização de componente genérico pode encontrar exatamente esse limite. Mantenha o provedor genérico antes de construir o cartão, em vez de presumir que uma existencial sempre possa ser convertida novamente em uma especialização persistente. Consulte SE-0352: Abertura implícita de existenciais.
Escolha o armazenamento conforme o momento em que o conteúdo deve ser produzido
O exemplo chama makeContent() durante cada inicialização de TileCard e armazena o valor da visualização retornado. Ele não estabelece um cache persistente entre inicializações posteriores do componente. Manter as duas propriedades demonstra a relação de tipos solicitada, mas um componente que precise apenas da visualização resultante poderia omitir o provedor armazenado.
Se o componente deve solicitar conteúdo sempre que seu body for avaliado, mantenha apenas o provedor. A declaração de componente a seguir substitui a original e deve ser usada com o protocolo e o app acima:
@MainActor
struct TileCard<Provider: TileProvider>: View {
private let provider: Provider
init(provider: Provider) {
self.provider = provider
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(provider.title)
.font(.headline)
provider.makeContent()
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.12))
}
}
Ambos os designs preservam o tipo associado. A diferença decorre diretamente do local da chamada do método: inicialização no primeiro design, avaliação do corpo no segundo. Escolha esse momento deliberadamente e evite que a produção de conteúdo dependa de ser chamada exatamente uma vez.
Use este guia de decisão ao adaptar o exemplo:
| Requisito do componente | Estratégia de declaração | Relação de tipos |
|---|---|---|
| Armazenar o provedor e a visualização que ele produz | Provedor genérico mais Provider.Content |
Preserva a identidade do provedor e seu tipo de conteúdo |
| Produzir conteúdo durante a avaliação do corpo | Armazenar o provedor genérico | Preserva o tipo associado sem armazenar um valor de conteúdo separado |
| Armazenar provedores com um tipo de conteúdo comum conhecido | Considere uma existencial restrita | Preserva a igualdade de conteúdo especificada enquanto oculta a identidade do provedor |
| Apagar deliberadamente implementações diferentes de visualização | Use AnyView em um limite escolhido |
Armazena o tipo do invólucro em vez de expor o tipo original da visualização |
Tipos associados primários eliminam a necessidade de genéricos?
Os tipos associados primários oferecem uma forma mais curta de expressar certas restrições de igualdade de tipos. Se você controla o protocolo, este cabeçalho alternativo torna Content primário; os demais requisitos do protocolo permanecem iguais:
// Alternative protocol header, requiring Swift 5.7 or later.
@MainActor
protocol TileProvider<Content> {
associatedtype Content: View
var title: String { get }
func makeContent() -> Content
}
Com essa declaração, any TileProvider<Text> descreve uma existencial cujo tipo de conteúdo associado é conhecido como Text. Diferentes implementações de provedor podem compartilhar esse tipo de armazenamento apenas se suas conformidades expuserem exatamente essa igualdade de conteúdo. A sintaxe restringe um tipo associado; ela não transforma o protocolo em uma estrutura genérica. Consulte SE-0346 e SE-0353.
Isso não combina tipos de visualização arbitrários e desconhecidos em um único tipo concreto preservado. Em particular, CircleTile expõe um resultado de conteúdo opaco, então os chamadores não podem simplesmente tratá-lo como Text. Use o componente genérico original quando o conteúdo associado do provedor precisar permanecer irrestrito.
As existenciais restritas foram implementadas no Swift 5.7, e a SE-0353 identifica explicitamente limitações de execução e de compatibilidade com versões anteriores. Portanto, o suporte do compilador, por si só, não basta para estabelecer a compatibilidade de implantação desse design opcional. Verifique os avisos de disponibilidade do toolchain de destino antes de adotá-lo. O exemplo principal não depende do armazenamento de existenciais restritas. Consulte a discussão sobre ABI na SE-0353.
O que verificar quando a compilação falha?
Acompanhe o tipo desde o requisito do protocolo até a propriedade armazenada antes de alterar o layout:
- Confirme que o protocolo declara um tipo associado restrito a
View. - Confirme que o componente tem um parâmetro de provedor genérico nomeado, restrito a esse protocolo.
- Declare o conteúdo armazenado como
Provider.Content, correspondendo ao tipo de retorno do método. - Verifique se um elemento pai já converteu o provedor para
any TileProvider. - Se adicionar um modificador antes de armazenar o conteúdo, confira seu tipo de resultado: a visualização modificada talvez já não seja
Provider.Content. - Verifique os diagnósticos de isolamento de ator separadamente dos diagnósticos de tipo associado.
A Apple documenta que os modificadores de visualização envolvem uma visualização em outra visualização. Consequentemente, atribuir provider.makeContent().padding() a uma propriedade declarada como Provider.Content altera o contrato que a atribuição precisa satisfazer. Armazene o conteúdo original e aplique os modificadores de apresentação em body, ou reformule o tipo armazenado para descrever o resultado modificado. Consulte View.
Os construtores também têm limites de versão. A documentação atual da Apple descreve o comportamento de ViewBuilder para o Xcode 26 e versões anteriores e, em seguida, apresenta um comportamento diferente para a construção de conteúdo a partir do Xcode 27. O exemplo evita um requisito de construtor personalizado: cada provedor retorna uma única expressão explícita. Se você o ampliar com APIs de provedores que usam construtores, siga a documentação da versão do Xcode com que realmente compila. Consulte ViewBuilder.
Se optar deliberadamente por AnyView, leve em conta o comportamento documentado: mudar o tipo da visualização encapsulada destrói a hierarquia antiga e cria uma nova. Essa é uma decisão de design separada da preservação de um tipo associado no armazenamento. A Apple documenta isso em AnyView.
Para este componente, mantenha a relação visível no limite de armazenamento: um Provider genérico, um resultado de Provider.Content e uma propriedade armazenada desse mesmo tipo. Assim, você poderá alterar o layout interno de um provedor enquanto deixa o compilador propagar seu tipo de visualização associado pelo componente.
Fontes
- Generics — Swift.org
- Tipos de protocolo opacos e encaixotados — Swift.org
- View — Apple
- Body — Apple
- App — Apple
- ViewBuilder — Apple
- AnyView — Apple
- SE-0244: Tipos de resultado opacos — Swift Evolution
- SE-0346: Requisitos leves de igualdade de tipos para tipos associados primários — Swift Evolution
- SE-0352: Abertura implícita de existenciais — Swift Evolution
- SE-0353: Tipos existenciais restritos — Swift Evolution