Asigna una apariencia predeterminada a un componente genérico de SwiftUI añadiendo un inicializador restringido a un tipo de contenido concreto. Conserva un inicializador independiente con un builder para la personalización. En este recorrido se construye un panel reutilizable para una ventana de macOS y se explica cuándo la inferencia de argumentos predeterminados de Swift 5.7 puede simplificar la interfaz.

Define las dos llamadas que debe admitir tu componente

La tarea del lector es sencilla: crear un panel de escritorio que se vea completo al llamarlo solo con un título y que permita a otra pantalla proporcionar su propio contenido. El título, el espaciado y el borde que lo rodean deben ser coherentes en ambos casos.

La interfaz prevista es DesktopPanel(title: "Workspace") para la presentación predeterminada y DesktopPanel(title: "Workspace") { Text("Three sample items") } para el contenido personalizado. Estas llamadas crean especializaciones distintas del mismo componente genérico.

Los parámetros genéricos de Swift representan tipos concretos. Una restricción como Content: View exige conformidad; un requisito de igualdad de tipos como Content == DefaultPanelContent selecciona un tipo específico. La referencia del lenguaje documenta ambos requisitos y la resolución de sobrecargas de inicializadores mediante restricciones. Parámetros y argumentos genéricos

Así, el componente tiene dos responsabilidades claras: su body se encarga de la apariencia compartida del panel, y su inicializador determina el contenido que se coloca dentro. El contenido predeterminado tiene un tipo de vista con nombre para que el inicializador de conveniencia pueda referirse a una especialización estable y legible.

Crea un ejemplo completo para macOS

Para este recorrido, usa un destino de app SwiftUI para macOS con un destino de implementación de macOS 11 o posterior y un compilador Swift 5.7 o posterior. La versión mínima de macOS permite usar la estructura App y WindowGroup que aparece a continuación; Apple documenta WindowGroup como el contenedor de escena para la jerarquía de vistas de una ventana. WindowGroup

Sustituye el punto de entrada generado de la app por este archivo de código fuente completo y elimina el archivo generado ContentView.swift si declara otro ContentView. Mantén exactamente una declaración de app @main en el destino. Todos los valores mostrados a continuación son ejemplos locales sintéticos.

import SwiftUI

struct DefaultPanelContent: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("Sample workspace")
                .font(.headline)
            Text("Choose a layout to begin.")
                .foregroundColor(.secondary)
        }
    }
}

struct DesktopPanel<Content: View>: View {
    private let title: String
    private let content: Content

    init(
        title: String,
        @ViewBuilder content: () -> Content
    ) {
        self.title = title
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(title)
                .font(.title2)

            Divider()

            content
        }
        .padding(16)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.08))
        .cornerRadius(10)
        .overlay(
            RoundedRectangle(cornerRadius: 10)
                .stroke(Color.gray, lineWidth: 1)
        )
    }
}

extension DesktopPanel where Content == DefaultPanelContent {
    init(title: String) {
        self.init(title: title) {
            DefaultPanelContent()
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            DesktopPanel(title: "Default panel")

            DesktopPanel(title: "Custom panel") {
                HStack {
                    Text("Sample items")
                    Spacer()
                    Text("3")
                        .font(.headline)
                }

                Text("Layout: Compact")
                    .foregroundColor(.secondary)
            }
        }
        .padding(24)
        .frame(minWidth: 420, minHeight: 300)
    }
}

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

Compila y ejecuta el destino de macOS. El resultado previsto son dos paneles con el mismo estilo exterior: el primero contiene el texto del espacio de trabajo predeterminado, y el segundo contiene una fila con un recuento seguida de una descripción del diseño. Este entorno no dispone de un compilador Swift ni del SDK de Apple, así que se inspeccionó el ejemplo, pero no se pudo compilar aquí.

Las pautas de Apple para vistas personalizadas respaldan esta composición: declara un tipo que conforme a View, implementa su body y combina vistas integradas y personalizadas en una jerarquía. También explican que los modificadores devuelven vistas modificadas, por lo que el estilo compartido debe aplicarse alrededor del contenido del panel. Declarar una vista personalizada

Por qué el inicializador restringido resuelve el tipo genérico

El inicializador personalizado recibe un cierre que devuelve Content. La llamada a content() construye el valor que se almacena en el panel. El cierre se consume durante la inicialización, así que esta implementación no lo almacena ni requiere @escaping.

Para la llamada sin builder, la extensión ofrece init(title:) específicamente cuando Content es DefaultPanelContent. Ese requisito aporta la información de tipo que falta. Luego, su implementación delega en el inicializador con builder para que la inicialización de las propiedades se haga en un solo lugar.

El valor predeterminado resultante tiene el tipo DesktopPanel<DefaultPanelContent>. El tipo de contenido del panel personalizado se infiere a partir de su cierre. Los llamadores no tienen que escribir ese tipo, que puede ser complicado.

Una consecuencia útil es que una instancia predeterminada ya está especializada. No puedes asignar un panel con un tipo de contenido distinto a la misma variable solo porque ambos valores muestran paneles. Si la interfaz necesita presentaciones alternativas, crea una rama basada en builders en la vista que los contiene.

El atributo builder permite usar en el ejemplo el cierre personalizado con varias instrucciones. Permite que HStack y el Text que le sigue formen parte del contenido construido; no reemplaza el requisito propio del componente, Content: View. Apple documenta cómo el builder procesa varias expresiones y contenido condicional. ViewBuilder

¿Cuándo puede un único inicializador con builder y valor predeterminado sustituir a la sobrecarga?

Swift 5.7 implementó SE-0347, que permite que la expresión predeterminada concreta de un argumento omitido infiera un tipo genérico. En este componente, eso ofrece una alternativa más breve. SE-0347: Inferencia de tipos a partir de expresiones predeterminadas

Con un compilador Swift 5.7 o posterior, sustituye el inicializador con builder dentro de DesktopPanel por el siguiente fragmento y elimina toda la extensión restringida. Conserva la importación, las propiedades almacenadas, DefaultPanelContent, body y el código de la app del ejemplo completo.

// Replacement initializer inside DesktopPanel.
init(
    title: String,
    @ViewBuilder content: () -> Content = {
        DefaultPanelContent()
    }
) {
    self.title = title
    self.content = content()
}

Ahora, si se omite el cierre, Content se infiere a partir de DefaultPanelContent(). Si se proporciona un cierre, se infiere a partir de ese argumento. Esto es inferencia de expresiones predeterminadas, no un argumento de tipo predeterminado en la declaración del componente.

La propuesta limita esta inferencia: el parámetro genérico afectado debe aparecer en un solo lugar de la lista de parámetros, y los requisitos de igualdad de tipos relacionados deben cumplir sus reglas. Aquí, Content solo aparece en el parámetro builder. Reutilizarlo en otro parámetro del inicializador puede invalidar este enfoque. Diseño detallado de SE-0347

Elige la sobrecarga cuando el contenido predeterminado necesite valores de entrada del inicializador. Por ejemplo, puedes construir dentro de init(title:) un subtítulo predeterminado derivado de title y pasarlo al inicializador con builder. La expresión de un argumento predeterminado no puede capturar el title proporcionado como si fuera un cierre creado dentro del cuerpo de ese inicializador.

Requisito del componente Interfaz adecuada
Contenido predeterminado fijo y compilador Swift 5.7 o posterior Un parámetro builder con valor predeterminado es conciso.
Contenido predeterminado construido a partir de otros valores de entrada del inicializador Usa un inicializador restringido y construye el valor dentro de él.
Varias presentaciones predeterminadas con nombre Usa sobrecargas explícitas del inicializador con etiquetas y restricciones claras.
Un tipo almacenado explícito para la especialización predeterminada Escribe DesktopPanel<DefaultPanelContent> o introduce un alias de tipo.

No intentes expresar esta interfaz como struct DesktopPanel<Content: View = DefaultPanelContent>. La gramática actual de parámetros genéricos de Swift no ofrece esa sintaxis de tipo predeterminado. Las opciones compatibles en este caso son la inicialización restringida y la inferencia de expresiones predeterminadas. Parámetros y argumentos genéricos

Conserva la interfaz en distintas versiones de builder

La documentación actual de Apple distingue entre Xcode 26 y versiones anteriores, y Xcode 27 y posteriores. Con la cadena de herramientas más reciente, ViewBuilder construye contenido independiente del tipo; Apple introduce ContentBuilder, un alias de tipo para ViewBuilder, como la forma de escribir este comportamiento unificado. Esta distinción depende de la cadena de herramientas de compilación, no simplemente del destino de implementación de la app. ViewBuilder, ContentBuilder

El ejemplo conserva @ViewBuilder para mantener la compatibilidad con cadenas de herramientas anteriores. Al adoptar la sintaxis más reciente en Xcode 27 o posterior, cambia ese atributo del parámetro a @ContentBuilder. Conserva Content: View: el panel sigue necesitando contenido que pueda colocar en una jerarquía de vistas.

Evita escribir la especialización predeterminada como un tipo anidado de pila y tupla. La nota de migración de Apple documenta un cambio de TupleView a TupleContent en builders con varias expresiones que puede romper las restricciones que nombran esas estructuras generadas. En su lugar, el ejemplo restringe la extensión a DefaultPanelContent; su diseño interno permanece tras body: some View. TN3211: Resolución de incompatibilidades del código fuente de SwiftUI para State y ContentBuilder

Este contenedor con nombre también facilita las modificaciones habituales. Añadir una tercera línea al contenido predeterminado cambia su implementación sin obligarte a reescribir un tipo concreto extenso en el inicializador de conveniencia.

Diagnostica el contrato antes de cambiar el diseño

Si el componente no compila, identifica primero qué parte de la interfaz carece de información de tipo. Cambiar el relleno o envolver todo en otra pila no resolverá la falta de una especialización genérica.

  1. La llamada sin builder no puede inferir Content. Confirma que exista init(title:) restringido o que el inicializador con valor predeterminado se compile con Swift 5.7 o posterior.
  2. No se puede asignar un valor predeterminado concreto a Content. Comprueba que la asignación se realice dentro de la extensión restringida. Conformar a View no hace que DefaultPanelContent sea intercambiable con cualquier Content posible.
  3. Se rechazan varias expresiones personalizadas. Confirma que el parámetro de cierre tenga el atributo builder.
  4. Un tipo explícito rechaza el contenido personalizado. La anotación DesktopPanel<DefaultPanelContent> fija deliberadamente el tipo de contenido. Elimina esa anotación cuando la llamada deba inferir otra especialización.
  5. Una actualización de la cadena de herramientas rompe una restricción basada en tuplas. Sustituye la dependencia de la estructura generada por el builder por una vista predeterminada con nombre, o sigue la migración documentada por Apple para esa restricción concreta.

Por último, mantén ligero el builder. En esta implementación se ejecuta cada vez que se inicializa el panel; no garantiza que se ejecute una sola vez. Apple señala que SwiftUI puede reinicializar las vistas y recomienda evitar trabajo significativo en sus inicializadores. Declarar una vista personalizada

Usa el inicializador restringido como punto de partida explícito: hace visible la especialización predeterminada y permite inferir fácilmente el contenido personalizado. Adopta el inicializador único con valor predeterminado cuando los requisitos del compilador y las restricciones de inferencia se ajusten a tu API. Cualquiera de los dos enfoques permite que el panel de macOS se encargue de su apariencia compartida mientras los llamadores proporcionan solo el contenido que necesitan.

Fuentes