Efectos visuales: gradientes, sombras, máscaras y materiales
ShapeStyle como álgebra de relleno, gradientes y su espacio de interpolación, el coste real de una sombra, enmascarado por alfa y luminancia, modos de mezcla separables y no separables, y los materiales del sistema con su vibrancia.
El acabado visual de una app no se decide en la elección de colores sino en el entendimiento del pipeline de composición: qué se rellena, contra qué se mezcla, en qué espacio de color se interpola y cuántas pasadas fuera de pantalla cuesta cada efecto. SwiftUI expone ese pipeline mediante un puñado de abstracciones muy bien elegidas —ShapeStyle, máscaras, modos de mezcla y materiales— que parecen decorativas y son en realidad el modelo de composición completo. Dominarlas es la diferencia entre imitar un efecto y saber por qué funciona, cuánto cuesta y cómo se comporta en modo oscuro, con transparencia reducida o sobre un fondo que se mueve.
- Usar
ShapeStylecomo un álgebra de rellenos: colores, gradientes, jerarquías y materiales. - Controlar la interpolación de gradientes y evitar bandas y mezclas turbias.
- Aplicar máscaras por alfa y por luminancia, y entender el grupo de composición.
- Elegir modos de mezcla y materiales del sistema conociendo su coste y su semántica.
ShapeStyle: un álgebra de rellenos
Todo lo que puede rellenar una superficie en SwiftUI conforma a un mismo protocolo. Color, los cuatro tipos de gradiente, los materiales, los estilos jerárquicos como secondary, el color de acento tint y las imágenes en mosaico mediante ImagePaint son intercambiables allí donde se espera un estilo. Esa uniformidad es lo que permite escribir código que no sabe con qué se pintará:
struct Insignia<Fondo: ShapeStyle>: View {
let fondo: Fondo
var body: some View {
Text("PRO")
.padding(.horizontal, 10).padding(.vertical, 4)
.background(fondo, in: .capsule)
.foregroundStyle(.white.shadow(.drop(radius: 1, y: 1)))
}
}
Dos detalles de peso. Primero, los estilos se componen: shadow aplicado a un estilo produce otro estilo, de modo que la sombra puede vivir en el relleno en lugar de en la vista, incluida la variante interior. Segundo, cuando necesitas heterogeneidad en tiempo de ejecución existe AnyShapeStyle, que borra el tipo a cambio de una indirección; conviene reservarlo para las fronteras del diseño y no salpicarlo por todas partes.
Los gradientes se declaran por paradas. La forma corta a partir de un color, la versión explícita con posiciones y las variantes radial, angular y elíptica cubren casi todo; la malla de gradientes permite superficies bidimensionales con control por vértice:
LinearGradient(stops: [
.init(color: .indigo, location: 0.0),
.init(color: .purple, location: 0.55),
.init(color: .pink, location: 1.0),
], startPoint: .topLeading, endPoint: .bottomTrailing)
AngularGradient(colors: [.red, .yellow, .green, .red], center: .center)
MeshGradient(width: 3, height: 3,
points: rejilla, // nueve vertices en espacio unitario
colors: paleta) // un color por vertice
La malla merece atención aparte porque cambia la naturaleza del degradado: en vez de interpolar a lo largo de un eje o de un radio, define una superficie bidimensional donde cada vértice aporta color y posición, y desplazar esos vértices con el tiempo produce fondos orgánicos que antes exigían vídeo o imágenes. El precio es que se rasteriza como cualquier otro relleno: una malla animada a pantalla completa es fill rate puro, y conviene medirla en el dispositivo más modesto que soportes antes de darla por buena.
Un degradado entre dos colores saturados y complementarios puede pasar por un gris sucio si se interpola en un espacio no lineal, y mostrar bandas visibles en superficies grandes con poca variación. Dos remedios: añadir paradas intermedias elegidas a mano para forzar el camino cromático deseado, y componer sobre un lienzo en modo extendedLinear cuando el degradado es el protagonista.
Sombras y el coste de una pasada extra
Una sombra proyectada no es un rectángulo difuminado detrás de la vista: es la silueta alfa del contenido, desplazada, desenfocada y compuesta debajo. Eso implica que el sistema debe rasterizar el contenido fuera de pantalla antes de poder desenfocarlo, y ese salto es el coste real.
tarjeta
.compositingGroup() // una silueta, no una por hijo
.shadow(color: .black.opacity(0.25), radius: 12, y: 6)
Circle()
.fill(.orange.shadow(.inner(radius: 6, y: 3))) // sombra dentro del relleno
compositingGroup es la pieza que más se olvida: sin él, un contenedor con varios hijos puede acabar generando una sombra por hijo, multiplicando pasadas y produciendo un resultado visualmente sucio en los solapes. Como regla operativa, aplica la sombra lo más cerca posible de una forma simple y bien definida, evita radios enormes en superficies grandes —el coste del desenfoque crece con el área y con el radio— y desconfía de las sombras dentro de listas que se desplazan, donde el gasto se paga en cada fotograma.
Máscaras y modos de mezcla
Enmascarar es multiplicar el alfa del contenido por el de otra vista. clipShape recorta con una geometría; mask admite cualquier vista como máscara, incluido un degradado, lo que da desvanecidos progresivos sin imágenes:
texto
.mask(LinearGradient(colors: [.black, .clear],
startPoint: .leading, endPoint: .trailing))
ZStack {
Rectangle().fill(.blue)
Circle().blendMode(.destinationOut) // agujero real
}
.compositingGroup()
El segundo ejemplo revela la regla que gobierna toda la mezcla: un modo de mezcla actúa contra el resultado acumulado del grupo de composición que lo contiene. Sin compositingGroup, ese agujero se abriría contra todo lo que hay detrás en la ventana, no contra el rectángulo azul. Comprender dónde empieza y acaba el grupo explica el noventa por ciento de los efectos que “no funcionan”.
Separables
multiply, screen, overlay y softLight operan canal a canal. Son los que oscurecen, aclaran o dan contraste.
No separables
hue, saturation, color y luminosity mezclan atributos perceptuales completos; sirven para teñir conservando la luz.
Porter-Duff
sourceAtop, destinationOut y plusLighter gestionan la geometría del alfa: recortar, perforar y acumular luz.
flowchart TD A[Contenido de la vista] --> B[Grupo de composicion] B --> C[Mascara multiplica el alfa] C --> D[Modo de mezcla contra el fondo del grupo] D --> E[Filtros como sombra o desenfoque] E --> F[Composicion final en pantalla] style B fill:#cba6f7,color:#11111b style D fill:#89b4fa,color:#11111b
Materiales del sistema
Los materiales no son colores translúcidos: son un desenfoque del contenido que hay detrás, más una capa de tinte y saturación calibrada por Apple para cada nivel de grosor. Desde el más tenue al más opaco cubren una escala de cinco pasos, más variantes semánticas para barras y menús.
contenido
.background(.regularMaterial, in: .rect(cornerRadius: 20))
.overlay(alignment: .top) {
Text("Encima del material")
.foregroundStyle(.secondary) // vibrancia, no gris fijo
}
Sobre un material, los estilos jerárquicos —primary, secondary, tertiary, quaternary— activan vibrancia: el texto no se pinta con un gris constante sino que mezcla con el fondo desenfocado para mantener legibilidad en cualquier contexto. Usar un color literal encima de un material es el error más común y el que peor envejece al cambiar de tema.
Quedan dos consideraciones. La primera es de coste: cada material implica capturar y desenfocar el fondo, así que apilarlos o animarlos sobre superficies grandes se paga caro. La segunda es de accesibilidad: con la opción de reducir transparencia activada, el sistema sustituye el material por una superficie opaca, y tu diseño debe seguir siendo legible en ese estado, lo que en la práctica significa no confiar en el desenfoque para separar contenido crítico.
La razón por la que las interfaces de Apple se sienten coherentes no es que usen degradados bonitos, sino que cada recurso visual codifica una relación espacial consistente. Una sombra afirma que algo está por encima; un material afirma que algo es una capa translúcida sobre un contenido que sigue existiendo debajo; la vibrancia afirma que el texto pertenece a esa capa y no al fondo. Cuando esos recursos se usan por su apariencia y no por su significado, el resultado tiene el síntoma inconfundible de la interfaz amateur: sombras en elementos que no flotan, materiales sobre fondos planos donde no hay nada que dejar entrever, mezclas que producen colores imposibles bajo iluminación real. La disciplina consiste en preguntarse siempre qué está afirmando el efecto sobre la estructura de la pantalla, y en verificar esa afirmación en los cuatro estados que no puedes ignorar: claro, oscuro, transparencia reducida y contraste aumentado.
- Construye una tarjeta con relleno de gradiente y sombra proyectada, y añade
compositingGrouppara comprobar la diferencia en los solapes. - Sustituye la sombra proyectada por una sombra interior aplicada al estilo de relleno y describe cuándo es preferible cada una.
- Haz un desvanecido de texto con
masky un degradado que va de negro a transparente. - Perfora un agujero en una forma con
destinationOuty verifica que el grupo de composición delimita el efecto. - Pon contenido sobre
regularMaterial, usa estilos jerárquicos para el texto y prueba la app con la transparencia reducida activada.