Declare o conteúdo fornecido pelo chamador como (Input) -> Content, com Content restrito a View. Isso permite que um componente do macOS forneça um valor tipado enquanto os chamadores escolhem sua apresentação. O contrato exige que a entrada seja aceita; não pode exigir que ela seja usada.

Este tutorial cria uma pequena prévia de amostras de cores para desktop. O componente reutilizável controla o tamanho e fornece uma configuração sintética à closure de renderização do chamador. O chamador desenha a amostra sem gerenciar o estado interno do componente.

Qual interface expressa esse requisito?

Use um parâmetro genérico nomeado para a view retornada e um tipo concreto para a entrada do componente. No exemplo abaixo, a assinatura essencial é @ViewBuilder content: @escaping (SwatchInput) -> Content, dentro de uma declaração com a restrição Content: View.

A entrada e a saída têm funções distintas. SwatchInput define os valores fornecidos pelo componente. Content representa o tipo concreto de view produzido pela closure de cada chamador. Parâmetros genéricos nomeados permitem que uma declaração expresse e reutilize essas relações entre tipos, conforme descrito no capítulo Generics do Swift.

A Apple usa esse padrão de interface em GeometryReader: seu inicializador aceita uma closure do tipo (GeometryProxy) -> Content. O contêiner fornece o proxy, e o chamador cria conteúdo a partir dele. Esse é um precedente útil para um componente personalizado que fornece sua própria configuração. Consulte a documentação da Apple sobre GeometryReader e init(content:).

Escolha o contrato de acordo com a tarefa real:

Requisito Interface adequada
O chamador fornece conteúdo sem entrada do componente () -> Content
O componente fornece uma configuração específica (SwatchInput) -> Content
O contêiner recebe um valor cujo tipo varia entre usos Um parâmetro genérico nomeado Input, com uma closure correspondente (Input) -> Content
O compilador precisa comprovar que o chamador usa a configuração A assinatura de uma closure não expressa esse requisito

Essas escolhas decorrem dos tipos de função do Swift: os tipos dos parâmetros e o tipo de retorno descrevem o que pode ser passado e retornado. Eles não descrevem como a implementação deve usar seus argumentos. Consulte a seção Function Type da referência da linguagem Swift.

Crie uma prévia completa de amostras de cores para macOS

Use um destino de app macOS com SwiftUI. O exemplo usa a grafia estabelecida @ViewBuilder e destina-se ao Swift 5.7 ou posterior, com destino de implantação macOS 12 ou posterior. Ele evita macros de prévia e nomes mais recentes para construtores de conteúdo. O Swift e o SDK da Apple não estão disponíveis no ambiente de pesquisa, portanto, este programa foi inspecionado, mas não foi compilado nem executado aqui.

Em um projeto de app SwiftUI do Xcode, substitua a declaração de app gerada pelo programa completo a seguir. Remova a declaração ContentView gerada se ela duplicar esta. Mantenha exatamente um ponto de entrada do app marcado com @main. A ajuda do Xcode da Apple explica como começar com um projeto SwiftUI ou adicionar um arquivo SwiftUI a um projeto existente.

import SwiftUI

struct SwatchInput {
    let title: String
    let side: CGFloat
    let color: Color
}

@MainActor
struct SwatchPanel<Content: View>: View {
    @State private var isLarge = false
    private let content: (SwatchInput) -> Content

    init(
        @ViewBuilder content: @escaping (SwatchInput) -> Content
    ) {
        self.content = content
    }

    private var input: SwatchInput {
        SwatchInput(
            title: "Sample A",
            side: isLarge ? 120 : 64,
            color: .blue
        )
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Toggle("Large sample", isOn: $isLarge)
            content(input)
        }
        .padding(24)
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        SwatchPanel { input in
            Text(input.title)
                .font(.headline)

            RoundedRectangle(cornerRadius: 12)
                .fill(input.color)
                .frame(width: input.side, height: input.side)

            if input.side == 120 {
                Text("Large preview")
                    .font(.caption)
            }
        }
        .frame(width: 300, height: 280, alignment: .topLeading)
    }
}

@main
@MainActor
struct TypedContentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

A exibição inicial esperada contém o controle Large sample, o título Sample A e um quadrado azul arredondado com 64 pontos de lado. Ativar o controle altera o tamanho fornecido para 120 pontos e acrescenta Large preview. Esses tamanhos e rótulos são valores locais ilustrativos, não resultados medidos.

O componente controla isLarge. Sua propriedade computada input reúne a seleção atual, e content(input) passa esse conjunto ao chamador. O SwiftUI gerencia o armazenamento do estado e atualiza as partes da hierarquia de views que dependem de um valor alterado, conforme documentado em State.

O chamador controla a apresentação: um título, uma forma e uma legenda opcional. Ele pode substituir essa organização por outra mantendo a mesma entrada tipada. Tanto o painel quanto seu chamador implementam body para compor suas interfaces, seguindo o modelo View documentado pela Apple.

Por que as anotações de builder e escaping são necessárias?

@ViewBuilder permite que o chamador escreva várias expressões de view e ramificações condicionais compatíveis em uma única closure. Neste exemplo, o chamador fornece duas views incondicionais e uma legenda condicional. O builder combina esse conteúdo em um resultado que satisfaz a restrição de retorno do componente. A Apple documenta várias views filhas e a construção condicional em ViewBuilder.

O builder não fornece SwatchInput. O parâmetro comum da closure declara a entrada, e content(input) fornece o argumento concreto. Se o inicializador aceitasse () -> Content, adicionar uma anotação de builder não o transformaria em uma closure que aceita uma configuração.

@escaping é necessária porque o inicializador armazena a closure em uma propriedade para invocá-la depois, a partir de body. O capítulo Closures do Swift explica que uma closure escapa quando pode ser chamada depois que a função que a recebeu retorna.

Para um componente cuja configuração já está finalizada na inicialização, outra opção de projeto é invocar imediatamente um builder não escapável e armazenar o valor Content resultante. Isso altera o momento em que a configuração é aplicada. Aqui, manter a closure deixa explícito o fluxo pretendido: derivar a entrada atual do estado do componente e, em seguida, criar conteúdo usando essa entrada.

Preserve o limite do builder do Xcode

A documentação atual da Apple distingue o Xcode 26 e anteriores do Xcode 27 e posteriores. Para as ferramentas anteriores, ela descreve ViewBuilder como algo que constrói views. No Xcode 27 e posteriores, descreve a construção sem restrição de tipo e recomenda o alias ContentBuilder para indicar esse comportamento mais amplo. Consulte ViewBuilder.

Ao manter uma base de código do Xcode 27 ou posterior, o inicializador pode usar @ContentBuilder. A Apple o documenta como um alias de tipo para ViewBuilder, com conformidade imposta pelos tipos de conteúdo que ele produz. Mantenha Content: View neste componente: a saída ainda se destina a uma hierarquia de views. Não presuma que a grafia mais recente exista em SDKs antigos só porque a documentação atual lista destinos de implantação mais antigos. Consulte ContentBuilder.

Por que some View não resolve todas as partes deste contrato?

some View é adequado para as propriedades body do exemplo. Ele oculta o tipo concreto do resultado, preservando a garantia de que o resultado é uma view. Em vez disso, a closure armazenada usa o parâmetro nomeado Content para que o componente possa se referir ao mesmo tipo de resultado em sua propriedade e em seu inicializador.

O Swift 5.7 introduziu declarações de parâmetros opacos por meio do SE-0341. Um parâmetro direto como _ value: some View introduz um parâmetro genérico sem nome cujo tipo concreto é determinado pelo chamador. Isso não significa que a implementação da função possa escolher uma view arbitrária e passá-la onde esse tipo sem nome é esperado. Consulte Opaque Parameter Declarations.

Em particular, um parâmetro de callback escrito como (some View) -> Content está fora das posições de parâmetro opaco aceitas e documentadas pelo SE-0341. A proposta proíbe explicitamente tipos opacos na posição de entrada do tipo de função de um callback. A discussão sobre a possível habilitação dessa sintaxe aparece entre as direções futuras, não como comportamento implementado. A referência atual de Types mantém essa restrição.

Para esta tarefa de desktop, a abordagem compatível é simples: torne o valor fornecido pelo componente um tipo concreto de configuração. Se outro contêiner reutilizável precisar preservar um tipo de entrada fornecido pelo chamador, nomeie esse tipo com um parâmetro genérico e mantenha-o de forma consistente no valor armazenado e no callback. Nenhuma das opções exige uma entrada opaca no callback.

O que o compilador realmente garante?

A assinatura garante uma forma chamável: um argumento SwatchInput e um resultado que satisfaz Content: View. Ela também fornece ao parâmetro da closure seu tipo inferido, permitindo que o chamador acesse input.title, input.side e input.color sem repetir uma anotação de tipo. O Swift documenta essa inferência a partir do contexto da closure em Closures.

Não é possível garantir que essas propriedades influenciem a exibição. Por exemplo, esta é uma expressão de substituição válida dentro de ContentView.body; ela aceita a configuração deliberadamente, mas a ignora:

SwatchPanel { _ in
    Text("Fixed preview")
}

Esse limite importa ao escrever os requisitos do componente. “O conteúdo do chamador aceita SwatchInput” é um contrato de tipo. “O conteúdo do chamador exibe o tamanho fornecido” é um requisito comportamental. Se exibir esse tamanho for essencial ao componente, inclua essa renderização no próprio componente e exponha uma área menor para personalização.

Use as verificações a seguir para diagnosticar problemas de integração. São etapas de inspeção derivadas da interface, não resultados relatados de testes do compilador:

Sintoma Verificação
Falta um argumento ao invocar content Passe a configuração: content(input).
Diagnóstico de closure escapável no inicializador Mantenha @escaping ao armazenar o callback.
Várias views irmãs causam falha no chamador Mantenha a anotação de builder adequada no parâmetro do inicializador.
Um callback com tipo explícito é rejeitado Confira se a entrada é SwatchInput, e não um tipo sem relação, como String.
A prévia ignora o controle de tamanho Confira se a renderização lê input.side.
O componente continua exibindo a configuração inicial Confira se o conteúdo foi criado uma única vez e armazenado, em vez de invocar o callback com a entrada atual.

Mantenha o contrato tão simples quanto a tarefa permitir. Uma configuração concreta e um resultado de view genérico definem um limite claro para este painel de amostras: o componente fornece os valores atuais, o chamador os recebe com todas as informações de tipo, e a apresentação escolhida continua fácil de substituir.

Fontes