Para receber conclusões de tipos diferentes em uma única tela SwiftUI para macOS, forneça a cada componente um adaptador genérico ou um closure tipado. O Swift não permite que a tela esteja em conformidade com o mesmo protocolo duas vezes, com tipos associados diferentes.
Este tutorial cria uma pequena prévia para desktop cujos botões aplicam um título sintético e uma quantidade de colunas. A tarefa é manter o contrato de conclusão de cada componente verificado pelo compilador e, ao mesmo tempo, permitir que uma única visualização pai receba ambos os valores.
Por que um tipo não pode adotar os dois contratos de conclusão?
O tipo associado de um protocolo pertence à sua conformidade. Se um protocolo de conclusão declara um tipo associado chamado Value, um tipo em conformidade precisa fornecer uma escolha consistente para esse tipo. Adicionar dois métodos de conclusão sobrecarregados não cria duas conformidades independentes. O guia da linguagem Swift explica como os tipos associados recebem seus tipos concretos quando um protocolo é adotado. Generics — Associated Types.
Por exemplo, uma tela não pode adotar um protocolo de conclusão uma vez com Value igual a TitleChoice e novamente com Value igual a LayoutChoice. Mover as declarações para extensões separadas não altera essa restrição. A SE-0143 preserva explicitamente a proibição de conformidade repetida, inclusive para conformidades condicionais cujas condições são disjuntas. Conditional conformances.
Os tipos associados primários não eliminam essa limitação. A sintaxe CompletionHandling<TitleChoice> expressa uma restrição sobre um tipo associado; ela não cria uma identidade de protocolo separada para cada argumento. A SE-0346 introduziu essa sintaxe de restrição no Swift 5.7. Lightweight same-type requirements for primary associated types.
O design compatível usado abaixo transfere a conformidade para um adaptador genérico. Há uma única declaração de conformidade, e a aplicação cria valores de adaptador especializados de maneiras diferentes para os dois componentes. A visualização pai mantém o estado resultante sem adotar o protocolo de conclusão.
Qual design de conclusão se encaixa no seu componente?
Use o guia de decisão a seguir antes de alterar a interface. Estas são recomendações de design derivadas das regras de tipos documentadas, não requisitos adicionais da linguagem Swift.
| Requisito do componente | Design sugerido | O que permanece explícito |
|---|---|---|
| Um callback, com uma interface sob seu controle | Um closure tipado | O tipo de entrada do callback |
| Uma API existente baseada em protocolo | Um adaptador genérico em conformidade | O protocolo e seu tipo associado |
| Um canal de eventos com várias alternativas nomeadas | Um enum com valores associados | Cada caso de evento tratado pelo receptor |
| Várias operações sem relação, com significados diferentes | Protocolos nomeados separados | Os requisitos distintos de cada operação |
Neste exemplo, suponha que o componente reutilizável já espere um manipulador baseado em protocolo. Um adaptador genérico preserva esse limite e mantém o código de tratamento da tela junto às atualizações de estado. Se o componente precisar apenas de um callback e não exigir um protocolo, um closure é o design mais simples.
Um método de conclusão genérico expressaria uma promessa diferente. Um método como complete<T>(_ value: T) precisa funcionar para todos os tipos permitidos por suas restrições. Ele não expressa o requisito mais específico de que cada componente entregue um tipo de resultado determinado. A documentação de genéricos do Swift descreve como funções genéricas operam com os tipos selecionados nos locais em que são chamadas. Generics — Generic Functions.
Crie o adaptador de conclusão tipado
Para este exemplo, use um destino de aplicação SwiftUI para macOS com um compilador Swift 5.7 ou posterior e um destino de implantação macOS 12 ou posterior. O requisito do compilador vem da declaração do tipo associado primário. O destino de implantação é uma base conservadora para este tutorial, não uma afirmação de que todas as APIs mostradas surgiram primeiro no macOS 12.
Os três arquivos abaixo formam juntos o exemplo completo. Adicione-os ao mesmo destino de aplicação, substituindo o ponto de entrada da aplicação e a visualização de conteúdo gerados para que o destino tenha apenas uma declaração @main. Não são necessários pacotes, recursos, arquivos de exemplo nem entradas externas.
Crie CompletionContracts.swift com estas declarações:
import SwiftUI
struct TitleChoice {
let text: String
}
struct LayoutChoice {
let columns: Int
}
@MainActor
protocol CompletionHandling<Value> {
associatedtype Value
func complete(_ value: Value)
}
@MainActor
struct CompletionAdapter<Output>: CompletionHandling {
typealias Value = Output
private let receive: @MainActor (Output) -> Void
init(_ receive: @escaping @MainActor (Output) -> Void) {
self.receive = receive
}
func complete(_ value: Output) {
receive(value)
}
}
@MainActor
struct CompletionButton<Value, Handler: CompletionHandling>: View
where Handler.Value == Value {
let title: String
let value: Value
let handler: Handler
var body: some View {
Button(action: {
handler.complete(value)
}) {
Text(title)
}
}
}
A restrição Handler.Value == Value conecta a saída do componente à entrada aceita pelo manipulador. Portanto, um botão que carrega um TitleChoice exige um manipulador cujo tipo associado seja TitleChoice. O alias de tipo explícito facilita a inspeção do tipo associado do adaptador.
O adaptador armazena o closure para invocá-lo posteriormente, por isso o inicializador marca esse parâmetro como @escaping. A documentação de closures do Swift exige essa anotação quando um closure pode sobreviver à chamada da função que o recebe. Closures — Escaping Closures.
O protocolo, o adaptador e a visualização usam @MainActor, e o callback armazenado preserva esse isolamento em seu tipo de função. Assim, o contexto de execução pretendido para a interface faz parte do contrato. A SE-0316 documenta tanto o isolamento por global actors quanto os tipos de função qualificados por global actors. Global actors.
A Apple documenta que Button recebe uma ação e um rótulo. Aqui, a ação invoca o manipulador com o valor já tipado do componente; Text(title) fornece o rótulo visível. Button.
Conecte dois tipos de resultado a uma única tela SwiftUI
Crie ContentView.swift. Os resultados do título e do layout têm destinos de estado separados, e cada closure do adaptador atualiza somente o destino correspondente.
import SwiftUI
@MainActor
struct ContentView: View {
@State private var previewTitle = "Sample Board"
@State private var columnCount = 1
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Component Preview")
.font(.headline)
Text(previewTitle)
Text("Columns: \(columnCount)")
HStack {
CompletionButton(
title: "Apply title",
value: TitleChoice(text: "Pebble Board"),
handler: CompletionAdapter<TitleChoice> { choice in
previewTitle = choice.text
}
)
CompletionButton(
title: "Apply layout",
value: LayoutChoice(columns: 3),
handler: CompletionAdapter<LayoutChoice> { choice in
columnCount = choice.columns
}
)
}
}
.padding(24)
.frame(minWidth: 420, minHeight: 180)
}
}
Crie TypedCompletionsApp.swift como o único ponto de entrada da aplicação:
import SwiftUI
@main
@MainActor
struct TypedCompletionsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
A visualização pai mantém seus valores iniciais em propriedades @State privadas. A Apple documenta que o SwiftUI gerencia esse armazenamento e atualiza as visualizações dependentes quando os valores mudam. A documentação atual também observa que compilações com Xcode 27 ou posterior usam a macro State; o estilo de declaração mostrado aqui segue a base anterior, que usa um wrapper de propriedade. State.
WindowGroup hospeda a hierarquia de visualizações da tela. A Apple documenta armazenamento de estado independente para cada janela criada pelo grupo; assim, neste exemplo, o estado pertence a cada janela de prévia. WindowGroup.
Há dois tipos de adaptador especializados: CompletionAdapter<TitleChoice> e CompletionAdapter<LayoutChoice>. Ambos vêm da única declaração de adaptador genérico. ContentView está em conformidade apenas com View; recebe os dois resultados pelos closures que fornece.
Verifique o comportamento e diagnostique incompatibilidades
Nenhum compilador Swift está instalado no ambiente de pesquisa, portanto os arquivos foram inspecionados, mas não foram compilados nem executados. Compile o destino de aplicação no seu Mac e use as verificações a seguir para confirmar o comportamento esperado do exemplo.
- Inicie o app. A prévia deve mostrar
Sample BoardeColumns: 1. - Clique em
Apply title. O título deve mudar paraPebble Board, enquanto a quantidade de colunas permanece igual. - Clique em
Apply layout. A quantidade deve mudar paraColumns: 3, enquanto o título escolhido permanece igual. - Clique novamente em qualquer um dos botões. Ele deve aplicar o mesmo valor sintético outra vez.
Para inspecionar o limite de tipos, altere temporariamente a especialização do manipulador do primeiro botão para CompletionAdapter<LayoutChoice> e deixe o corpo do closure vazio. Mantenha o valor desse botão como TitleChoice. Então, o componente exige que tipos incompatíveis sejam iguais por meio de Handler.Value == Value, e a compilação deve falhar. Depois, restaure o manipulador original.
Se o compilador informar conformidade repetida, procure outra declaração que faça o mesmo tipo nominal adotar CompletionHandling. Mantenha a única conformidade do adaptador genérico e crie outro valor de adaptador para a saída adicional. Duas extensões com restrições que declaram a mesma conformidade continuam sujeitas à restrição do Swift contra conformidade repetida. Conditional conformances — Multiple conformances.
Se o compilador rejeitar a sintaxe do tipo associado primário do protocolo, verifique a versão do compilador. Em um compilador mais antigo, o design do adaptador pode manter uma declaração comum de tipo associado. Este fragmento substitui apenas a declaração do protocolo; mantenha as demais declarações inalteradas:
@MainActor
protocol CompletionHandling {
associatedtype Value
func complete(_ value: Value)
}
Essa alteração elimina a dependência deste exemplo da sintaxe do Swift 5.7. Ela não elimina o requisito de compilador para @MainActor, introduzido no Swift 5.5, nem altera o destino de implantação do tutorial. O componente já usa uma cláusula genérica where, portanto não precisa da sintaxe de tipo associado primário no local de uso. Lightweight same-type requirements, Global actors.
O que esse contrato de conclusão garante?
O exemplo garante que os tipos dos dados correspondam em cada limite do componente. Sua implementação chama o manipulador uma vez por ativação do botão, de forma síncrona. Ela não implementa uma operação assíncrona, cancelamento, persistência nem um ciclo de vida de conclusão de uso único. Isso exigiria comportamento adicional e um contrato explícito.
O exemplo também usa valores locais conhecidos. Se um componente futuro aceitar quantidades arbitrárias de colunas, valide essa entrada antes de atualizar a prévia e defina como a rejeição será informada. A correspondência de um tipo Swift, por si só, não determina que um inteiro seja uma quantidade de colunas útil.
Para esta tarefa de desktop, mantenha um adaptador por ponto final tipado e deixe que a visualização pai seja proprietária do estado resultante. Assim, os dois componentes têm contratos de conclusão precisos por meio de uma única declaração de conformidade reutilizável, e o destino de cada resultado fica visível no local da chamada.
Fontes
- Conditional conformances — Swift Evolution
- Lightweight same-type requirements for primary associated types — Swift Evolution
- Generics — The Swift Programming Language
- Closures — The Swift Programming Language
- Global actors — Swift Evolution
- Button — Apple Developer Documentation
- State — Apple Developer Documentation
- WindowGroup — Apple Developer Documentation