Las previews de SwiftUI son especialmente útiles cuando una vista tiene varios estados relevantes: contenido vacío, datos cargados, errores, valores límite o distintas fases de un proceso. Hasta ahora, revisar todos esos casos solía obligarnos a crear varios bloques #Preview casi idénticos o a meter todas las variantes dentro de un VStack. Con Xcode 27 aparece una alternativa mucho más limpia: #Preview(arguments:).
Esta nueva variante de la macro permite pasar un array de valores y genera automáticamente una preview independiente para cada elemento. En lugar de construir manualmente cinco previews para cinco estados, podemos definir una sola fuente de escenarios y dejar que Xcode los presente como variantes dentro del canvas.
#Preview(
"Estados del episodio",
arguments: Episode.previewSamples
) { episode in
EpisodeRow(episode: episode)
}
La firma de la API deja bastante clara su intención. #Preview recibe un array [T] y el bloque de contenido recibe cada elemento de forma individual. Apple no exige que T implemente Identifiable, Hashable, Equatable ni CaseIterable, por lo que podemos utilizar enums, modelos de dominio o tipos creados expresamente para describir escenarios de preview.
struct Episode {
let title: String
let duration: Duration
let state: PlaybackState
}
enum PlaybackState {
case notStarted
case playing(progress: Double)
case paused(progress: Double)
case finished
case unavailable
}
Una práctica muy útil consiste en mantener los datos de preview junto al propio modelo, o en un archivo dedicado exclusivamente a previews. De esta forma, la lista de escenarios se convierte en un pequeño catálogo visual del componente.
extension Episode {
static let previewSamples: [Self] = [
.init(
title: "Cómo funciona Swift Concurrency",
duration: .seconds(2_940),
state: .notStarted
),
.init(
title: "Diseñando una app para visionOS",
duration: .seconds(3_480),
state: .playing(progress: 0.32)
),
.init(
title: "Una introducción práctica a SwiftData",
duration: .seconds(2_760),
state: .paused(progress: 0.71)
),
.init(
title: "Testing moderno en Swift",
duration: .seconds(3_120),
state: .finished
),
.init(
title: "Arquitecturas modulares en aplicaciones Apple",
duration: .seconds(4_020),
state: .unavailable
)
]
}
Antes de arguments:, una solución habitual era declarar una preview por cada caso. Funciona y sigue siendo perfectamente válida, pero empieza a generar bastante código repetido cuando el componente tiene muchos estados.
#Preview("Sin reproducir") {
EpisodeRow(episode: Episode.previewSamples[0])
}
#Preview("Reproduciendo") {
EpisodeRow(episode: Episode.previewSamples[1])
}
#Preview("Pausado") {
EpisodeRow(episode: Episode.previewSamples[2])
}
#Preview("Finalizado") {
EpisodeRow(episode: Episode.previewSamples[3])
}
#Preview("No disponible") {
EpisodeRow(episode: Episode.previewSamples[4])
}
Con #Preview(arguments:), esa repetición desaparece y la relación entre escenarios queda expresada directamente en el array. Además, el canvas no interpreta las variantes como cinco vistas dentro de una composición mayor, sino como previews independientes pertenecientes al mismo grupo.
#Preview(
"Estados del episodio",
traits: .sizeThatFitsLayout,
arguments: Episode.previewSamples
) { episode in
EpisodeRow(episode: episode)
.padding()
}
Esta diferencia es importante frente a otro patrón bastante común: usar ForEach dentro de una única preview. Un VStack con todos los casos puede ser estupendo para comparar rápidamente el diseño, pero para Xcode sigue existiendo una sola preview. Cada fila es simplemente una subvista de esa composición.
#Preview("Todos los estados") {
VStack(spacing: 16) {
ForEach(Episode.previewSamples.indices, id: \.self) { index in
EpisodeRow(episode: Episode.previewSamples[index])
}
}
.padding()
}
Con arguments:, Xcode puede mostrar las distintas variantes en el canvas como un conjunto y después abrir una de ellas individualmente. Esto resulta especialmente útil cuando queremos inspeccionar un estado concreto con más detalle o interactuar con sus controles sin perder el catálogo completo de escenarios.
Hay otro detalle importante: el nombre que Xcode muestra para cada variante. Si usamos modelos complejos, la representación por defecto del valor puede ser demasiado larga o poco útil. Para mejorar esos nombres podemos hacer que el tipo implemente CustomStringConvertible.
extension Episode: CustomStringConvertible {
var description: String {
switch state {
case .notStarted:
"Sin reproducir"
case let .playing(progress):
"Reproduciendo \(progress.formatted(.percent.precision(.fractionLength(0))))"
case let .paused(progress):
"Pausado \(progress.formatted(.percent.precision(.fractionLength(0))))"
case .finished:
"Finalizado"
case .unavailable:
"No disponible"
}
}
}
Conviene utilizar descripciones cortas, estables y suficientemente distintas entre sí. Si dos argumentos terminan mostrando exactamente el mismo nombre, identificar el escenario correcto en una cuadrícula de previews vuelve a ser innecesariamente difícil.
El uso ideal de arguments: es representar un eje finito de estados significativos de entrada. Por ejemplo, una tarjeta puede tener estados normal, destacado, agotado y deshabilitado. También podemos incluir algún caso límite concreto si aporta valor visual: un título extremadamente largo, una cantidad especialmente grande o un mensaje de error localizado.
Sin embargo, no conviene convertir el array de argumentos en una matriz de todas las condiciones posibles. Aspectos como el modo claro u oscuro, el tamaño de Dynamic Type, la orientación, el dispositivo o determinadas configuraciones del entorno encajan mejor en PreviewTrait o en grupos de previews separados. Mezclar estados de negocio y condiciones de entorno acaba provocando una explosión combinatoria muy difícil de revisar.
#Preview(
"Estados · tamaño grande",
traits: .sizeThatFitsLayout,
arguments: Episode.previewSamples
) { episode in
EpisodeRow(episode: episode)
.environment(\.dynamicTypeSize, .accessibility2)
.padding()
}
Los argumentos tampoco sustituyen al estado mutable de una preview. Su objetivo es proporcionar el valor inicial que genera cada variante. Si necesitamos interactuar con un Binding, cambiar un @State o modificar un modelo observable directamente desde el Canvas, @Previewable sigue siendo la herramienta adecuada.
#Preview("Control de velocidad") {
@Previewable @State var speed = 1.0
PlaybackSpeedPicker(speed: $speed)
}
Internamente, @Previewable permite declarar propiedades dinámicas dentro del propio bloque #Preview. La macro genera una vista auxiliar y convierte esas declaraciones en propiedades de esa vista. Por eso arguments: y @Previewable no compiten entre sí: resuelven problemas distintos. Los argumentos crean variantes a partir de entradas diferentes y @Previewable permite que una variante tenga estado mutable durante la interacción.
La nueva API pertenece a Xcode 27 porque la sobrecarga Preview(_:traits:arguments:body:) forma parte de las nuevas APIs de previews pero está disponibles desde iOS 26, macOS 27, tvOS 26, watchOS 26 y visionOS 26, por lo que conviene comprobar los requisitos definitivos antes de depender de este comportamiento durante el ciclo beta.
Esto no significa necesariamente que una aplicación tenga que elevar su deployment target a iOS 26. Si el proyecto sigue soportando versiones anteriores, podemos limitar la disponibilidad de esa preview concreta.
@available(iOS 26.0, *)
#Preview(
"Estados del episodio",
traits: .sizeThatFitsLayout,
arguments: Episode.previewSamples
) { episode in
EpisodeRow(episode: episode)
}
Xcode permite además expandir la macro con Expand Macro, algo especialmente interesante en este caso. La expansión muestra que la infraestructura de registro de previews sigue apoyándose en APIs anteriores y que la nueva sobrecarga se protege mediante comprobaciones de disponibilidad. Es una buena demostración de cómo las macros pueden ofrecer sintaxis nueva sin obligar al resto del código de producción a compartir exactamente los mismos requisitos de disponibilidad.
En componentes medianos o complejos, este cambio favorece una forma de trabajar muy útil: tratar las previews como un catálogo de estados del diseño. En vez de pensar en una preview como una simple captura del caso feliz, podemos mantener una colección deliberada de escenarios representativos y revisarlos juntos cada vez que cambiamos el componente.
#Preview(arguments:) no cambia radicalmente lo que podemos representar con SwiftUI, porque esos escenarios ya podían construirse manualmente. Lo que cambia es la ergonomía: elimina repetición, da entidad propia a cada variante en el Canvas y convierte un array de datos de ejemplo en una pequeña colección navegable de estados visuales. Para componentes que viven en más de un estado, es una de esas mejoras pequeñas que pueden terminar formando parte del flujo de trabajo diario.