Para construir um componente genérico do SwiftUI, exija seu inicializador em um protocolo ou injete um closure que o retorne. Este tutorial usa as duas abordagens para exibir o mesmo item sintético em uma janela do macOS, com limites explícitos de compilador e SDK.
O que um componente genérico precisa garantir?
Suponha que um app para macOS exiba um pequeno cartão de item. Você quer um painel reutilizável que construa o cartão a partir de um item, permitindo que os chamadores escolham a apresentação. A questão central é quem controla a construção: a interface declarada do componente ou o chamador que fornece o componente.
Uma restrição genérica dá à implementação acesso às capacidades especificadas por essa restrição. Escrever Component: View estabelece que o componente é uma view do SwiftUI; isso não estabelece que o componente tenha um inicializador chamado init(item:). O capítulo sobre genéricos do Swift explica as restrições de protocolo, enquanto a documentação de View da Apple descreve a propriedade body exigida pela view.
Consequentemente, este trecho é intencionalmente inválido:
// Invalid fragment: View does not require init(item:).
@MainActor
func construct<Component: View>(item: Item) -> Component {
Component(item: item)
}
Adicionar esse inicializador a um componente concreto não o torna disponível por meio de todo parâmetro genérico restrito apenas a View. A API reutilizável precisa de um contrato explícito de construção. Use esta tabela de decisão antes de alterar o componente:
| Requisito de construção | Interface adequada | Motivo |
|---|---|---|
| Todo componente participante aceita o mesmo item | Protocolo que exige init(item:) |
A construção faz parte da interface da família de componentes. |
| Os chamadores escolhem configuração ou modificadores adicionais | Closure de fábrica que retorna o componente genérico | A construção permanece no ponto de chamada. |
| O chamador já tem um componente pronto | Armazenar o valor do componente genérico | A construção já aconteceu. |
| A apresentação alterna entre um conjunto finito de layouts | Compor as alternativas dentro de uma view do SwiftUI | A API externa pode manter uma estrutura de view concreta. |
Esta tabela é um auxílio de projeto derivado das regras documentadas da linguagem. As duas primeiras opções são implementadas juntas abaixo para que seu comportamento possa ser comparado diretamente.
Crie um exemplo completo para macOS
Use um target de app para macOS no Xcode com SwiftUI e uma versão mínima de implantação do macOS 11 ou posterior. O exemplo foi escrito para um compilador Swift 6 com o SDK da Apple correspondente. As anotações explícitas @MainActor exigem Swift 5.5 ou posterior, portanto o código não se destina a compiladores anteriores. A Apple documenta WindowGroup como disponível a partir do macOS 11 e como um contêiner para a hierarquia de views de um app. Consulte WindowGroup.
Substitua o arquivo de ponto de entrada do app gerado pelo programa completo a seguir. Verifique se o target contém apenas uma declaração de app @main. O arquivo ContentView gerado pode permanecer sem uso. Não são necessários pacotes, recursos externos nem dados de exemplo baixados.
import SwiftUI
struct Item {
let title: String
let detail: String
}
@MainActor
protocol ItemComponent: View {
init(item: Item)
}
@MainActor
struct ItemCard: ItemComponent {
let item: Item
init(item: Item) {
self.item = item
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(item.title)
.font(.headline)
Text(item.detail)
}
.padding()
.background(Color.gray.opacity(0.12))
.cornerRadius(8)
}
}
@MainActor
struct ContractPanel<Component: ItemComponent>: View {
let item: Item
init(item: Item, component: Component.Type) {
self.item = item
}
var body: some View {
Component(item: item)
}
}
@MainActor
struct FactoryPanel<Component: View>: View {
private let makeComponent: @MainActor () -> Component
init(make: @escaping @MainActor () -> Component) {
self.makeComponent = make
}
var body: some View {
makeComponent()
}
}
@MainActor
struct DemoView: View {
private let item = Item(
title: "Sample tile",
detail: "Blue circle on a neutral background"
)
var body: some View {
VStack(alignment: .leading, spacing: 18) {
Text("Initializer contract")
ContractPanel(item: item, component: ItemCard.self)
Text("Injected factory")
FactoryPanel {
ItemCard(item: item)
.padding(.leading, 12)
}
Text("Alternative factory output")
FactoryPanel {
Text(item.title + ": " + item.detail)
}
}
.padding(24)
.frame(width: 420)
}
}
@main
@MainActor
struct ComponentDemoApp: App {
var body: some Scene {
WindowGroup {
DemoView()
}
}
}
Execute o target do app para macOS. O resultado esperado é uma janela com duas apresentações em cartão e uma apresentação em texto simples. O segundo cartão tem preenchimento adicional à esquerda fornecido pelo chamador. Estes são exemplos sintéticos do comportamento da API, não resultados medidos. Não há um compilador Swift instalado no ambiente de pesquisa, portanto o código foi inspecionado, mas não foi compilado nem executado aqui.
As anotações de ator principal tornam explícito o limite de construção da interface. A declaração atual de View da Apple é anotada com @MainActor e @preconcurrency; a documentação também explica como o isolamento é herdado quando a conformidade aparece na declaração original de um tipo. Consulte View.
Como funciona o contrato do inicializador?
ItemComponent estende View e adiciona exatamente um requisito de construção: init(item:). O Swift permite declarar requisitos de inicializador diretamente em protocolos. Um tipo que esteja em conformidade fornece a implementação; o protocolo declara a assinatura sem o corpo do inicializador. Consulte Protocolos: requisitos de inicializador.
Esse requisito torna válido Component(item: item) dentro de ContractPanel. O argumento component: Component.Type fornece um tipo concreto para a inferência genérica: passar ItemCard.self determina que o tipo do componente deste painel é ItemCard. O painel não precisa armazenar o metatipo porque seu body constrói por meio do parâmetro de tipo genérico.
Esse contrato é útil quando todo componente deve poder ser construído a partir da mesma entrada. Por exemplo, uma família de apresentações compactas e expandidas de itens, mantida pelo app, poderia compartilhar esse requisito. Assim, um chamador pode mudar o tipo de componente selecionado sem reescrever a expressão de construção do painel.
A desvantagem aparece quando uma implementação precisa de configuração adicional. Exigir init(item:) significa que cada componente em conformidade precisa oferecer esse caminho de construção, mesmo que seu inicializador preferido também aceite uma legenda ou configuração de layout. Se essa informação adicional pertence ao chamador, a abordagem de fábrica abaixo costuma expressar o requisito de forma mais direta.
Para implementações baseadas em classes, um inicializador que satisfaz um requisito de protocolo precisa ser required, a menos que a classe seja final. Uma subclasse que também substitua um inicializador designado correspondente da superclasse pode precisar de required e override. Structs como ItemCard não usam required. Essas distinções estão documentadas em Protocolos.
Quando a construção deve passar para uma fábrica?
FactoryPanel aceita uma função do tipo @MainActor () -> Component. Sua única garantia é que chamar a função produz o tipo de componente selecionado. O closure pode invocar um inicializador, capturar configurações ou retornar uma view com modificadores aplicados.
O programa demonstra a diferença prática: a fábrica pode retornar um ItemCard com preenchimento ou retornar Text, sem exigir que nenhum dos tipos resultantes adote ItemComponent. A Apple explica que os modificadores de view podem envolver a entrada em outra view, por isso é útil deixar o compilador inferir o tipo resultante da fábrica aqui. Consulte View.
O inicializador marca o closure com @escaping porque o painel o armazena para invocá-lo posteriormente. O capítulo sobre closures do Swift documenta closures escapáveis e sua capacidade de capturar valores do contexto ao redor. Neste exemplo, o closure captura o item sintético usado pela view externa.
Escolha deliberadamente as entradas da fábrica. Uma fábrica sem argumentos significa que o chamador captura tudo o que é necessário para a construção. Se o próprio painel seleciona um item, uma fábrica parametrizada pode expressar essa responsabilidade com mais clareza. A definição a seguir substitui a definição do componente para ser usada junto com o Item existente; ela não é um segundo app completo:
@MainActor
struct ItemFactoryPanel<Component: View>: View {
let item: Item
private let makeComponent: @MainActor (Item) -> Component
init(
item: Item,
make: @escaping @MainActor (Item) -> Component
) {
self.item = item
self.makeComponent = make
}
var body: some View {
makeComponent(item)
}
}
Com essa definição, o painel fornece o item e o chamador fornece a apresentação. Limite o trabalho da fábrica à produção do valor da view: o contrato de construção da amostra não promete execução única, armazenamento persistente nem cache.
As fábricas precisam de um construtor de resultados?
As fábricas do programa completo são closures comuns. Cada uma retorna uma expressão, portanto não é necessária uma anotação de construtor personalizada. Se forem necessárias várias views filhas, retornar um único VStack é suficiente para esta API; a stack contém as views filhas.
Um construtor de resultados torna-se útil quando o próprio parâmetro da fábrica deve aceitar uma sequência declarativa de expressões ou conteúdo condicional. O Swift Evolution registra o recurso padronizado de construtores de resultados como implementado no Swift 5.4. Consulte SE-0289: Construtores de resultados.
Preserve o limite do SDK ao adotar a sintaxe de construtores. A documentação atual da Apple descreve @ViewBuilder para parâmetros que produzem views no Xcode 26 e anteriores. Para o Xcode 27 e posteriores, descreve a construção independente de tipo e recomenda o alias ContentBuilder para indicar esse comportamento. Consulte ViewBuilder e ContentBuilder. O closure comum usado aqui evita exigir esse alias mais recente.
Por que tipos opacos ou existenciais não substituem o contrato?
some View oculta um tipo de resultado subjacente específico e preserva sua identidade. Funciona bem para as propriedades body deste programa. Não adiciona init(item:) à interface View. Um valor encapsulado escrito como any View oculta seu tipo subjacente de outra maneira e não preserva a mesma identidade de tipo estático. Consulte Tipos de protocolo opacos e encapsulados.
Não transforme isso em uma afirmação genérica de que valores existenciais nunca podem ser passados a funções genéricas. A SE-0352 introduziu existenciais abertos implicitamente no Swift 5.7, permitindo que chamadas elegíveis vinculem um parâmetro genérico ao tipo subjacente de um existencial. Ela também documenta restrições, incluindo resultados que incorporam o tipo aberto em um wrapper genérico, quando o resultado não pode ser representado após o apagamento de tipo. Consulte Existenciais abertos implicitamente.
Para esta tarefa do painel, basta preservar o tipo concreto do componente por meio de um parâmetro genérico. Não é necessário introduzir armazenamento existencial apenas para permitir que os chamadores escolham como construir uma view.
Como diagnosticar um erro de construção?
Siga a expressão que falha e inspecione o contrato disponível naquele ponto:
- Se
Component(item:)falhar, verifique se a restrição genérica inclui um protocolo que exija exatamente essa assinatura de inicializador. - Se o compilador não conseguir inferir
Component, forneça um metatipo concreto, comoItemCard.self, ou uma fábrica com uma expressão de retorno concreta. - Se um componente precisar de configuração específica do chamador, passe a construção para a fábrica em vez de adicionar requisitos não relacionados ao protocolo.
- Se armazenar um closure gerar um diagnóstico de closure escapável, verifique a anotação
@escapingdo inicializador. - Se a construção gerar um diagnóstico de isolamento de ator, verifique se tanto o ponto de chamada quanto o tipo de função da fábrica expressam o limite pretendido do ator principal.
- Se um existencial causar um erro de resultado genérico, examine as restrições de tipo de resultado na SE-0352 antes de presumir que uma atualização do compilador elimina a limitação.
Para uma família de componentes compartilhada no macOS, exija o inicializador quando a construção uniforme fizer parte da interface. Use uma fábrica injetada quando o chamador precisar montar e configurar a apresentação. Ambos os caminhos tornam explícita a garantia de construção ausente e preservam o tipo concreto de componente de que o painel precisa.
Fontes
- Protocolos — The Swift Programming Language, Swift.org.
- Genéricos — The Swift Programming Language, Swift.org.
- Closures — The Swift Programming Language, Swift.org.
- Tipos de protocolo opacos e encapsulados — The Swift Programming Language, Swift.org.
- View — Documentação do desenvolvedor da Apple.
- WindowGroup — Documentação do desenvolvedor da Apple.
- ViewBuilder — Documentação do desenvolvedor da Apple.
- ContentBuilder — Documentação do desenvolvedor da Apple.
- Construtores de resultados — Swift Evolution.
- Existenciais abertos implicitamente — Swift Evolution.