Para permitir que um único delegate lide com vários tipos de componentes SwiftUI, torne o método de callback genérico e mantenha o componente independente do tipo concreto do delegate. Este guia cria um pequeno seletor de blocos para macOS que preserva o tipo de conteúdo do emissor sem um requisito recursivo de Self.
Escolha o contrato do callback antes de escolher a sintaxe
A tarefa do leitor é específica: criar um componente SwiftUI reutilizável cujo conteúdo possa variar, enquanto um delegate recebe callbacks de seleção de todas as especializações de conteúdo compatíveis. A demonstração usa dois blocos sintéticos — um texto e um círculo — e exibe o identificador do bloco selecionado.
Swift define delegação como uma forma baseada em protocolos de uma classe ou estrutura transferir responsabilidades para outra instância. Um delegate pode receber uma ação junto com seu emissor, como mostra o exemplo de delegação do guia da linguagem. Protocols
Para este componente, o contrato pretendido é: “Para cada tipo de conteúdo que esteja em conformidade com View, o delegate pode receber um bloco que contenha esse conteúdo.” Coloque o parâmetro de tipo no método de callback. As funções genéricas do Swift selecionam seus argumentos de tipo concretos em cada chamada, enquanto um tipo associado é estabelecido por uma conformidade de protocolo. São compromissos diferentes. Generics
| Requisito | Formato de interface adequado | Consequência |
|---|---|---|
| Um delegate lida com todos os tipos de conteúdo compatíveis | Um método de callback genérico | Cada callback pode ter um tipo de conteúdo diferente. |
| Cada delegate lida com um tipo de conteúdo fixo | Um tipo associado | Cada conformidade fornece um tipo de conteúdo. |
| O callback precisa apenas do identificador do bloco | Um callback que recebe o identificador | O callback não depende do tipo de renderização. |
Esta tabela é um auxílio de design derivado dessas regras da linguagem. Escolha a primeira linha quando preservar o emissor tipado fizer parte da API. Escolha a terceira quando o identificador do emissor fornecer tudo de que o receptor precisa; nesse caso, carregar um componente inteiro acrescentaria uma dependência desnecessária.
Remova a dependência do próprio tipo do delegate
Um design fortemente acoplado pode tornar o componente genérico em relação ao delegate e, em seguida, referir-se a esse componente como Component<Self> dentro do protocolo do delegate. Aqui, Self significa o tipo concreto que acabará em conformidade com o protocolo. Tipos associados também pertencem a esse tipo que está em conformidade. Declarations
Esse formato faz com que a verificação do callback dependa de uma especialização do componente que, por sua vez, depende do delegate. Para saber se uma declaração recursiva específica é aceita, é preciso examinar suas restrições completas e os diagnósticos do compilador. Seria incorreto afirmar que Swift rejeita toda relação recursiva entre protocolos: a SE-0157 adicionou restrições recursivas de tipos associados no Swift 4.1. Support recursive constraints on associated types
Para a tarefa escolhida, não é necessário incluir o tipo concreto do delegate no tipo do componente. O design abaixo dá a Tile um parâmetro genérico, Content. Seu delegate armazenado é any TileDelegate, e o requisito do protocolo introduz seu próprio parâmetro Content no nível do método. O callback preserva o tipo de conteúdo do emissor sem expressá-lo por meio do Self do delegate.
A forma any denota explicitamente um valor existencial: o valor armazenado pode conter uma instância de um tipo que esteja em conformidade com o protocolo do delegate. A SE-0335 introduziu essa forma no Swift 5.6. Usar um existencial para o delegate não exige apagar o tipo do conteúdo do bloco para AnyView. Introduce existential any
Crie um seletor de blocos completo para macOS
Use um destino de app SwiftUI para macOS com um compilador Swift 6, como o compilador fornecido com o Xcode 16 ou posterior. Defina o destino de implantação do macOS como 11.0 ou posterior para o armazenamento @StateObject deste exemplo. A Apple documenta que StateObject está disponível a partir do macOS 11.0. StateObject
Substitua a declaração de app gerada e a view inicial por este único arquivo-fonte. Garanta que o destino contenha apenas uma declaração de app @main; Swift permite apenas uma abordagem de ponto de entrada do programa. Declarations
import SwiftUI
import Combine
@MainActor
protocol TileDelegate: AnyObject {
func didChoose<Content: View>(_ tile: Tile<Content>)
}
@MainActor
struct Tile<Content: View>: View {
let id: String
let delegate: any TileDelegate
let content: Content
init(
id: String,
delegate: any TileDelegate,
@ViewBuilder content: () -> Content
) {
self.id = id
self.delegate = delegate
self.content = content()
}
var body: some View {
Button(action: {
delegate.didChoose(self)
}) {
VStack(spacing: 8) {
content
.frame(width: 100, height: 50)
Text("Choose \(id)")
}
.padding(8)
}
}
}
@MainActor
final class SelectionModel: ObservableObject, TileDelegate {
@Published private(set) var selection = "No tile selected"
func didChoose<Content: View>(_ tile: Tile<Content>) {
selection = "Selected: \(tile.id)"
}
}
@MainActor
struct ChooserView: View {
@StateObject private var model = SelectionModel()
var body: some View {
VStack(spacing: 16) {
HStack(spacing: 16) {
Tile(id: "text", delegate: model) {
Text("Sample")
}
Tile(id: "circle", delegate: model) {
Circle()
}
}
Text(model.selection)
}
.padding(24)
.frame(minWidth: 340, minHeight: 180)
}
}
@main
@MainActor
struct DelegateDemoApp: App {
var body: some Scene {
WindowGroup {
ChooserView()
}
}
}
Este é um exemplo completo de app, incluindo importações, delegate, componente, modelo e ponto de entrada. Ele foi inspecionado, mas não compilado nem executado aqui porque o ambiente disponível não tem compilador Swift nem SDK do macOS.
A API Button da Apple aceita uma ação e um rótulo que produz uma view. Neste exemplo, clicar em um bloco invoca o delegate na ação do botão; avaliar body constrói a interface sem enviar um evento de seleção. Button
Compile e execute o app em um Mac. Inicialmente, o status deve ser No tile selected. Clicar em Choose text deve resultar em Selected: text; clicar em Choose circle deve resultar em Selected: circle. Estes são os resultados esperados do exemplo, não resultados de execução relatados.
Entenda o que o exemplo preserva
O primeiro bloco tem o tipo Tile<Text>, e o segundo tem o tipo Tile<Circle>. Ambos usam a mesma instância de SelectionModel. Seu callback genérico satisfaz o requisito do protocolo para qualquer um dos tipos de conteúdo, ao mesmo tempo que lê a propriedade comum id. Esse é o benefício prático de colocar o parâmetro genérico no método.
O receptor também pode acessar tile.content como Content. No entanto, dentro da implementação genérica, a restrição declarada garante apenas as capacidades de View. O código que exigir uma operação específica de texto precisará de outro contrato ou de informações adicionais; o callback não pode presumir que todo valor de conteúdo seja Text. Isso decorre do modelo genérico do Swift, baseado em restrições. Generics
O componente implementa body com some View, seguindo o padrão documentado pela Apple para views personalizadas. A declaração atual de View também traz isolamento de ator principal. O exemplo marca explicitamente o protocolo e os tipos voltados à interface com @MainActor para que o callback e a atualização do modelo compartilhem esse isolamento. View
@StateObject fornece armazenamento estável para o modelo durante todo o ciclo de vida da identidade da view que o contém. Alterações na seleção publicada pelo modelo atualizam a view de status dependente. Mover a criação do modelo para uma expressão computada comum deixaria de representar essa propriedade por meio de StateObject. StateObject
O exemplo armazena fortemente um delegate obrigatório. Seu modelo armazena apenas a string da seleção, portanto, o grafo de objetos ilustrado não contém uma referência de volta a um bloco. Se mais tarde você fizer o modelo reter emissores, reveja a propriedade. O guia da linguagem da Apple demonstra um delegate vinculado a uma classe com armazenamento fraco quando essa relação exige isso. Protocols
Quando um tipo associado é o melhor contrato
Suponha que um receptor deva aceitar apenas blocos de texto. Um protocolo alternativo poderia declarar associatedtype Content: View e exigir func didChoose(_ tile: Tile<Content>). Isso expressa um tipo de conteúdo por conformidade. Tornar Content um tipo associado primário oferece uma sintaxe de restrição conveniente; isso não transforma o requisito em um método que aceite todos os tipos de conteúdo. A SE-0346 introduziu tipos associados primários no Swift 5.7. Lightweight same-type requirements for primary associated types
Essa alternativa seria uma versão diferente da API. Você também alteraria a propriedade delegate e o parâmetro do inicializador do componente para o existencial restrito correspondente, como any FixedTileDelegate<Content>, e mudaria cada receptor para fornecer seu tipo associado fixo e o callback não genérico correspondente. Uma conformidade restrita a texto já não poderia atender ao bloco de círculo por meio dessa mesma conformidade.
A SE-0353 adicionou tipos existenciais restritos no Swift 5.7, permitindo vincular os tipos associados primários de um existencial a tipos especificados. Vincular um tipo associado ainda descreve uma relação fixa. Isso não fornece uma família de implementações de callback para tipos de conteúdo arbitrários. Constrained Existential Types
Mantenha o suporte do compilador separado do suporte à implantação. A SE-0353 observa explicitamente alterações de runtime que não podem ser implantadas retroativamente em versões existentes dos sistemas operacionais. Antes de adotar a alternativa com um destino de implantação antigo, verifique os diagnósticos de disponibilidade no destino real. O exemplo completo acima usa um existencial de delegate comum e não exige armazenamento de existencial restrito. Constrained Existential Types
Diagnostique uma adaptação com falha em uma ordem útil
- Verifique o escopo pretendido do callback. Se um delegate precisar lidar com blocos de texto e de círculo, sua testemunha deve preservar a assinatura do método genérico. Um método que aceite apenas
Tile<Text>não satisfaz esse requisito. - Verifique os parâmetros genéricos do componente. Neste design,
Tiledepende deContent; seu tipo não inclui o tipo concreto do delegate. - Verifique o isolamento de ator. Mantenha os callbacks da interface e o modelo observável ilustrado no ator principal. Chamadas de outro contexto de isolamento devem respeitar esse limite.
- Verifique onde ocorre o evento. Envie a seleção na ação do botão, não ao calcular
bodynem ao inicializar uma view. - Verifique a propriedade do modelo. Preserve
@StateObjectna view que o contém e evite introduzir uma segunda instância do modelo para o callback.
Se uma adaptação ainda falhar, reduza-a a um bloco e uma implementação de delegate, depois restaure o segundo tipo de conteúdo. Essa sequência distingue um problema de testemunha de protocolo de um problema de composição de views. Os dois tipos de conteúdo diferentes do exemplo são um teste útil do contrato: ambos devem chegar ao mesmo callback genérico sem conversões nem apagamento de tipo do conteúdo.
A decisão essencial de design é onde a variação de tipos deve ficar. Para um delegate que lida com todas as especializações do componente, coloque essa variação no método de callback e mantenha o tipo do componente independente da identidade concreta do delegate. A API resultante preserva um emissor tipado e mantém o fluxo de seleção no macOS suficientemente pequeno para ser inspecionado e ampliado.
Fontes
- Generics — The Swift Programming Language
- Protocols — The Swift Programming Language
- Declarations — The Swift Programming Language
- Support recursive constraints on associated types — Swift Evolution
- Introduce existential any — Swift Evolution
- Lightweight same-type requirements for primary associated types — Swift Evolution
- Constrained Existential Types — Swift Evolution
- View — Apple Developer Documentation
- StateObject — Apple Developer Documentation
- Button — Apple Developer Documentation