Para recibir finalizaciones de distintos tipos en una misma pantalla de SwiftUI para macOS, proporciona a cada componente un adaptador genérico o un cierre tipado. Swift no permite que la pantalla adopte el mismo protocolo dos veces con tipos asociados diferentes.

Este tutorial crea una pequeña vista previa de escritorio cuyos botones aplican un título y un número de columnas sintéticos. El objetivo es que el compilador compruebe el contrato de finalización de cada componente y, a la vez, permitir que una vista principal reciba ambos valores.

¿Por qué un tipo no puede adoptar ambos contratos de finalización?

El tipo asociado de un protocolo pertenece a su conformidad. Si un protocolo de finalización declara un tipo asociado llamado Value, el tipo que lo adopta debe proporcionar una elección coherente para ese tipo. Añadir dos métodos de finalización sobrecargados no crea dos conformidades independientes. La guía del lenguaje de Swift explica cómo los tipos asociados adquieren sus tipos concretos cuando se adopta un protocolo. Genéricos — Tipos asociados.

Por ejemplo, una pantalla no puede adoptar un mismo protocolo de finalización una vez con Value igual a TitleChoice y otra vez con Value igual a LayoutChoice. Trasladar las declaraciones a extensiones separadas no cambia esa restricción. SE-0143 mantiene explícitamente la prohibición de conformidad repetida, incluidas las conformidades condicionales con condiciones disjuntas. Conformidades condicionales.

Los tipos asociados primarios no eliminan este límite. La sintaxis CompletionHandling<TitleChoice> expresa una restricción sobre un tipo asociado; no crea una identidad de protocolo independiente para cada argumento. SE-0346 introdujo esta sintaxis de restricciones en Swift 5.7. Requisitos de igualdad de tipos ligeros para tipos asociados primarios.

El diseño compatible que se muestra a continuación traslada la conformidad a un adaptador genérico. Hay una sola declaración de conformidad, y la aplicación crea valores de adaptador especializados de forma distinta para los dos componentes. La vista principal es propietaria del estado resultante sin adoptar el protocolo de finalización.

¿Qué diseño de finalización se adapta a tu componente?

Usa esta guía de decisión antes de cambiar la interfaz. Son recomendaciones de diseño derivadas de las reglas de tipos documentadas, no requisitos adicionales del lenguaje Swift.

Requisito del componente Diseño sugerido Qué se mantiene explícito
Una llamada de retorno con una interfaz que controlas Un cierre tipado El tipo de entrada de la llamada de retorno
Una API existente basada en protocolos Un adaptador genérico que adopta el protocolo El protocolo y su tipo asociado
Un canal de eventos con varias alternativas con nombre Un enum con valores asociados Cada caso de evento que gestiona quien lo recibe
Varias operaciones no relacionadas con significados distintos Protocolos con nombres separados Los requisitos distintos de cada operación

Para este ejemplo, supongamos que el componente reutilizable ya espera un manejador basado en protocolos. Un adaptador genérico conserva ese límite y mantiene el código de gestión de la pantalla junto a las actualizaciones de estado. Si el componente solo necesita una llamada de retorno y no requiere un protocolo, un cierre es la opción más sencilla.

Un método de finalización genérico expresaría una promesa distinta. Un método como complete<T>(_ value: T) debe funcionar con todos los tipos permitidos por sus restricciones. No expresa el requisito más específico de que cada componente entregue un tipo de resultado determinado. La documentación de genéricos de Swift describe cómo las funciones genéricas operan con los tipos seleccionados en sus puntos de llamada. Genéricos — Funciones genéricas.

Crear el adaptador de finalización tipado

Usa un destino de aplicación SwiftUI para macOS con un compilador Swift 5.7 o posterior y un destino de implementación macOS 12 o posterior para este ejemplo. El requisito del compilador se debe a la declaración del tipo asociado primario. El destino de implementación es una base conservadora para este tutorial, no una afirmación de que todas las API mostradas aparecieran por primera vez en macOS 12.

Los tres archivos siguientes forman juntos el ejemplo completo. Añádelos al mismo destino de aplicación y reemplaza el punto de entrada y la vista de contenido generados, de modo que el destino tenga una sola declaración @main. No se necesitan paquetes, recursos, archivos de ejemplo ni entradas externas.

Crea CompletionContracts.swift con estas declaraciones:

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)
        }
    }
}

La restricción Handler.Value == Value conecta la salida del componente con la entrada aceptada por el manejador. Por tanto, un botón que lleva un TitleChoice requiere un manejador cuyo tipo asociado sea TitleChoice. El alias de tipo explícito facilita inspeccionar el tipo asociado del adaptador.

El adaptador guarda el cierre para invocarlo más adelante, así que su inicializador marca ese parámetro como @escaping. La documentación de cierres de Swift exige esta anotación cuando un cierre puede sobrevivir a la llamada de función que lo recibe. Cierres — Cierres escapables.

El protocolo, el adaptador y la vista usan @MainActor, y la llamada de retorno almacenada conserva ese aislamiento en su tipo de función. Así, el contexto previsto de ejecución de la interfaz forma parte del contrato. SE-0316 documenta tanto el aislamiento de actores globales como los tipos de función cualificados por actores globales. Actores globales.

Apple documenta que Button recibe una acción y una etiqueta. Aquí, la acción invoca el manejador con el valor ya tipado del componente; Text(title) proporciona su etiqueta visible. Button.

Conectar dos tipos de resultado a una misma pantalla de SwiftUI

Crea ContentView.swift. Los resultados del título y del diseño tienen destinos de estado separados, y cada cierre del adaptador actualiza únicamente su destino correspondiente.

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)
    }
}

Crea TypedCompletionsApp.swift como único punto de entrada de la aplicación:

import SwiftUI

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

La vista principal guarda sus valores iniciales en propiedades @State privadas. Apple documenta que SwiftUI administra este almacenamiento y actualiza las vistas dependientes cuando cambian los valores. La documentación actual también señala que las compilaciones con Xcode 27 o posterior usan la macro State en su lugar; el estilo de declaración mostrado aquí sigue la base anterior de contenedor de propiedades. State.

WindowGroup aloja la jerarquía de vistas de la pantalla. Apple documenta que cada ventana creada por el grupo tiene almacenamiento de estado independiente, por lo que el estado de este ejemplo pertenece a cada ventana de vista previa. WindowGroup.

Hay dos tipos de adaptador especializados: CompletionAdapter<TitleChoice> y CompletionAdapter<LayoutChoice>. Ambos proceden de una única declaración de adaptador genérico. ContentView solo adopta View; recibe los dos resultados mediante los cierres que proporciona.

Verificar el comportamiento y diagnosticar incompatibilidades

El entorno de investigación no tiene instalado un compilador de Swift, así que estos archivos se inspeccionaron, pero no se compilaron ni ejecutaron. Compila el destino de aplicación en tu Mac y luego usa las siguientes comprobaciones para verificar el comportamiento previsto del ejemplo.

  1. Inicia la aplicación. La vista previa debe mostrar Sample Board y Columns: 1.
  2. Haz clic en Apply title. El título debe cambiar a Pebble Board, mientras que el número de columnas permanece igual.
  3. Haz clic en Apply layout. El número debe cambiar a Columns: 3, mientras que el título elegido permanece igual.
  4. Vuelve a hacer clic en cualquiera de los botones. Debe volver a aplicar el mismo valor sintético.

Para inspeccionar el límite entre tipos, cambia temporalmente la especialización del manejador del primer botón a CompletionAdapter<LayoutChoice> y dale un cierre vacío. Deja el valor de ese botón como TitleChoice. El componente exige entonces que tipos incompatibles sean iguales mediante Handler.Value == Value, así que la compilación debe fallar. Después, restaura el manejador original.

Si el compilador informa de una conformidad repetida, busca otra declaración que haga que el mismo tipo nominal adopte CompletionHandling. Conserva la única conformidad del adaptador genérico y crea otro valor de adaptador para la salida adicional. Dos extensiones con restricciones que declaran la misma conformidad siguen sujetas a la restricción de Swift sobre conformidades repetidas. Conformidades condicionales — Varias conformidades.

Si el compilador rechaza la sintaxis de tipo asociado primario del protocolo, comprueba la versión del compilador. En un compilador anterior, el diseño del adaptador puede conservar una declaración ordinaria de tipo asociado. Este es un fragmento de reemplazo solo para la declaración del protocolo; deja las demás declaraciones sin cambios:

@MainActor
protocol CompletionHandling {
    associatedtype Value
    func complete(_ value: Value)
}

Ese cambio elimina la dependencia de este ejemplo de la sintaxis de Swift 5.7. No elimina el requisito de compilador para @MainActor, introducido en Swift 5.5, ni cambia el destino de implementación del tutorial. El componente ya usa una cláusula genérica where, así que no necesita sintaxis de tipo asociado primario en el punto de uso. Requisitos de igualdad de tipos ligeros, Actores globales.

¿Qué garantiza este contrato de finalización?

El ejemplo garantiza que el tipo del valor recibido coincida en el límite de cada componente. Su implementación llama al manejador una vez por cada activación del botón, de forma síncrona. No implementa una operación asíncrona, cancelación, persistencia ni un ciclo de vida de finalización de una sola ejecución. Eso requeriría comportamiento adicional y un contrato explícito.

El ejemplo también usa valores locales conocidos. Si en el futuro un componente acepta números de columnas arbitrarios, valida esa entrada antes de actualizar la vista previa y define cómo se informa del rechazo. Que los tipos de Swift coincidan no demuestra por sí solo que un entero sea un número de columnas útil.

Para esta tarea de escritorio, mantén un adaptador por cada punto de conexión tipado y deja que la vista principal sea propietaria del estado resultante. Así, los dos componentes tienen contratos de finalización precisos mediante una única declaración de conformidad reutilizable, y el destino de cada resultado queda visible en el punto de llamada.

Fuentes