Liquid Glass en iOS 26: cómo adaptar una app SwiftUI al nuevo lenguaje de diseño de Apple#
Cuando Apple presentó Liquid Glass en la WWDC 2025, la primera reacción de la mayoría de desarrolladores iOS no fue «¿cómo funciona esto?», sino «¿cuántas pantallas voy a tener que rehacer ahora?». Es una pregunta legítima. Liquid Glass no es un simple cambio de piel como el diseño plano de iOS 7: es un material con física propia. Refracta la luz, reacciona al movimiento del dispositivo y al tacto, se fusiona con los elementos vecinos y puede transformarse (morph) entre estados. SwiftUI trae un conjunto de API dedicado para esto, y si lo aplicas de forma puramente mecánica sobre una UI antigua, tu app termina pareciendo ajena junto a las pantallas del sistema.
He adaptado varias de mis apps a Liquid Glass, incluida MeteoHealth, y en este artículo reúno lo que realmente ha resultado útil: la API vigente en 2026 (con los ajustes de iOS 26.1 y 26.2), los errores que se repiten al migrar una UI personalizada, y qué le pasa a tu app cuando un usuario activa Reduce Transparency.
Qué es realmente Liquid Glass — no es solo otro blur#
Antes de iOS 26 teníamos los materiales — .ultraThinMaterial, .regularMaterial y el resto de la familia Material — un blur estático con opacidad configurable. Liquid Glass no es una evolución de los materiales, sino una capa de sistema independiente que:
- refracta y refleja el contenido que tiene debajo, en lugar de simplemente difuminarlo;
- reacciona al tacto y al puntero — un elemento se comprime ligeramente y se resalta al pulsarlo;
- se fusiona con los elementos glass vecinos cuando se acercan, y hace morph entre formas cuando cambia la jerarquía de vistas;
- se adapta automáticamente a lo que hay debajo — un fondo claro da un borde más contrastado, uno oscuro, un aspecto más transparente.
La regla arquitectónica clave de Apple: Liquid Glass es un material de la capa funcional — navegación, toolbars, controles, overlays transitorios — no de la capa de contenido. Una tarjeta de artículo, una foto en una galería, una lista de mensajes son contenido, y convertirlos en cristal no es buena idea aunque sea técnicamente posible. La segunda regla es no apilar cristal sobre cristal: si un elemento ya está sobre una navigation bar o un sheet del sistema, otro glassEffect encima produce una mancha turbia en vez de la profundidad que buscabas.
La nueva API: glassEffect, Glass y el orden de los modificadores#
El punto de entrada básico es el modificador .glassEffect(). Sin parámetros, envuelve una vista en una forma Capsule con la variante .regular:
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect()Puedes especificar una forma explícita (.rect(cornerRadius:), .circle, .capsule), y el comportamiento del material se configura mediante la estructura Glass: .regular es el cristal base, .tint(Color) añade un acento de color para elementos más prominentes, y .interactive() activa una reacción tipo resorte al tocar:
struct WeatherSummaryCard: View {
let temperature: String
let condition: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(condition)
.font(.headline)
Text(temperature)
.font(.system(size: 34, weight: .semibold, design: .rounded))
}
.padding(20)
.frame(maxWidth: .infinity, alignment: .leading)
.modifier(AdaptiveGlassBackground(cornerRadius: 24))
}
}
/// Applies Liquid Glass on iOS 26+, falls back to Material on older systems.
/// Note the order: layout modifiers (padding, frame) come first,
/// the glass effect is applied last, on top of the finished layout.
struct AdaptiveGlassBackground: ViewModifier {
let cornerRadius: CGFloat
func body(content: Content) -> some View {
if #available(iOS 26.0, *) {
content.glassEffect(
.regular.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
} else {
content.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
)
}
}
}El orden de los modificadores es determinante aquí. glassEffect debe ir después de los modificadores que definen el layout y la apariencia (padding, frame, font), no antes: el material mide los límites finales de la vista y necesita que el layout ya esté resuelto. Poner .glassEffect() antes de .padding() es una causa habitual de que "el cristal se recorte por el borde equivocado".
GlassEffectContainer, morphing y glassEffectID: estados dinámicos#
En cuanto tienes más de un elemento glass cerca uno de otro — por ejemplo, una barra de acciones rápidas — hay que envolverlos en un GlassEffectContainer. El contenedor establece una región de muestreo compartida y permite que los elementos vecinos se fusionen visualmente en lugar de simplemente superponerse:
enum QuickAction: String, CaseIterable, Identifiable {
case refresh, share, favorite
var id: String { rawValue }
var symbolName: String {
switch self {
case .refresh: return "arrow.clockwise"
case .share: return "square.and.arrow.up"
case .favorite: return "heart"
}
}
}
struct QuickActionsBar: View {
@State private var isExpanded = false
@Namespace private var glassNamespace
var body: some View {
GlassEffectContainer(spacing: 24) {
HStack(spacing: 24) {
Button {
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
isExpanded.toggle()
}
} label: {
Image(systemName: isExpanded ? "xmark" : "plus")
.frame(width: 56, height: 56)
}
.buttonStyle(.glass)
.glassEffectID("toggle", in: glassNamespace)
if isExpanded {
ForEach(QuickAction.allCases) { action in
Button {
// handle action
} label: {
Image(systemName: action.symbolName)
.frame(width: 56, height: 56)
}
.buttonStyle(.glass)
.glassEffectID(action.id, in: glassNamespace)
.glassEffectUnion(id: "expanded", namespace: glassNamespace)
}
}
}
}
}
}Dos detalles importan aquí. Primero, el valor spacing de GlassEffectContainer controla a qué distancia deben estar los elementos vecinos para empezar a fusionarse visualmente — cuanto menor el valor, más cerca deben estar las vistas. Segundo, glassEffectID junto con @Namespace es lo que convierte la aparición y desaparición de botones en un morph suave en lugar de un crossfade brusco: sin withAnimation envolviendo el cambio de isExpanded, el efecto simplemente no se activa. glassEffectUnion agrupa además elementos en una única superficie glass cuando se crean fuera de un HStack compartido — por ejemplo, dentro de un ForEach dinámico.
Fallback para versiones anteriores a iOS 26: dos lenguajes de diseño en una app#
Si tu app todavía tiene usuarios en versiones de iOS anteriores a la 26 — y la mayoría de productos reales los tendrán durante uno o dos años más — el fallback no es opcional, es parte obligatoria de la adaptación. El patrón correcto es no mantener dos árboles de vistas paralelos, sino envolver la decisión en un único modificador con #available, como en el ejemplo de AdaptiveGlassBackground anterior: .glassEffect(...) en iOS 26+ y .background(.ultraThinMaterial, in:) en sistemas más antiguos, usando la misma forma (RoundedRectangle frente a .rect(cornerRadius:)) para que la geometría de la tarjeta no cambie entre versiones de iOS.
Un error habitual en este paso es olvidar que .glassEffect() tiene un parámetro isEnabled, más cómodo que un if #available metido dentro de la propia vista: permite mantener una única implementación de vista y alternar el material con un flag calculado una sola vez, más arriba en el árbol.
Errores típicos al adaptar una UI personalizada#
Tras migrar varias apps, los mismos errores se repiten:
- Cristal sobre cristal. Una tarjeta personalizada con
glassEffectdentro de unsheetdel sistema o unNavigationStackque ya tiene una toolbar de cristal se convierte en una mancha gris opaca — el contraste y la legibilidad del texto caen. - Glass en la capa de contenido. La portada de un artículo, una foto de perfil, el fondo de un reproductor son contenido, no un elemento funcional; intentar "embellecerlos" con cristal suele hacer la interfaz menos legible, no más premium.
- Múltiples elementos glass independientes sin contenedor. Cada icono recibe su propio
glassEffect()sinGlassEffectContainer— el renderizado se encarece y la fusión visual que buscabas nunca ocurre. - Orden de modificadores incorrecto.
glassEffectantes depaddingoframe— el material mide los límites contra un layout aún sin terminar. - Ignorar Reduce Transparency. Un código que solo comprueba
#available(iOS 26, *)y nada más ignora que parte de tus usuarios reduce deliberadamente la transparencia de la interfaz.
Accesibilidad: Reduce Transparency, el modo Tinted y lo que no puedes saltarte#
Desde iOS 26.1, Apple dio a los usuarios control directo sobre Liquid Glass: el interruptor Reduce Transparency en Accesibilidad → Pantalla y Tamaño de Texto aumenta la opacidad del material, y en Ajustes → Pantalla y Brillo → Liquid Glass apareció un selector entre los modos "Clear" y "Tinted" — este último aumenta el contraste mediante un ligero oscurecimiento. iOS 26.2 añadió además un deslizador de transparencia para el reloj de la pantalla de bloqueo.
Para un desarrollador, esto significa que no hace falta desactivar manualmente glassEffect cuando accessibilityReduceTransparency está activo — el sistema ya aumenta la opacidad del material por sí solo. Lo que sí conviene tener en cuenta es ese valor de entorno a la hora de decidir si añadir .interactive(), porque la animación tipo resorte es un aspecto de accesibilidad distinto de la transparencia:
struct AccessibleGlassSurface<Content: View>: View {
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
let cornerRadius: CGFloat
@ViewBuilder var content: Content
var body: some View {
if #available(iOS 26.0, *) {
content
// Don't disable glassEffect manually when Reduce Transparency
// is on — iOS already increases frosting for the .regular
// variant. We only drop the extra .interactive() bounce,
// since motion is a separate accessibility concern.
.glassEffect(
reduceTransparency ? .regular : .regular.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
} else {
content.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
)
}
}
}Prueba esto en las vistas previas, no solo alternando el ajuste del sistema en el dispositivo una y otra vez: .environment(\.accessibilityReduceTransparency, true) en un #Preview ahorra bastante tiempo.
Matriz de API: qué cambiar durante la adaptación#
| Elemento de UI | Antes de iOS 26 | iOS 26 (Liquid Glass) |
|---|---|---|
| Tarjeta / panel | .background(.ultraThinMaterial, in: RoundedRectangle(...)) | .glassEffect(.regular, in: .rect(cornerRadius:)) |
| Botón de acción | ZStack personalizado con blur y sombra manuales | .buttonStyle(.glass) / .buttonStyle(.glassProminent) |
| Grupo de iconos cercanos | Material individual en cada uno | GlassEffectContainer + glassEffectUnion |
| Elemento que aparece/desaparece | .transition simple, ajeno a los vecinos | glassEffectID + @Namespace para el morph |
| Toolbar / tab bar accessory | Fondo personalizado, separación manual de grupos | Superficie glass automática + ToolbarSpacer |
Cómo se aplicó en la práctica: MeteoHealth y una checklist de adaptación#
En MeteoHealth, los elementos que realmente pasaron a ser de cristal fueron los funcionales: una barra de acciones rápidas en la pantalla principal, controles flotantes sobre los gráficos de bienestar, y un tab bar accessory con el resumen meteorológico actual — todo lo que está "por encima" del contenido, no lo que es contenido en sí. Las tarjetas de pronóstico y el historial de métricas se quedaron con un fondo normal: las pruebas con usuarios reales mostraron rápidamente que el cristal sobre datos numéricos densos empeora la legibilidad en lugar de mejorar la sensación de la interfaz.
La checklist final antes de publicar:
- El glass se aplica solo a la capa funcional — navegación, toolbars, controles transitorios
- No hay casos de "cristal sobre cristal" (comprobado contra sheets/NavigationStacks reales)
- Los elementos glass vecinos están envueltos en un
GlassEffectContainer -
glassEffectva después de los modificadores de layout, no antes - Existe un fallback
#available(iOS 26, *)aMaterialcon la misma geometría de forma - Se ha comprobado el comportamiento con
accessibilityReduceTransparencyen previews y en dispositivo - Se ha probado el modo Tinted en Ajustes → Pantalla y Brillo → Liquid Glass
- El morphing entre estados está envuelto en
withAnimation
Liquid Glass es uno de esos casos poco frecuentes en los que Apple no lanza un simple retoque estético, sino un material con física real y sus propias reglas de composición. Copiar los antiguos blurs de Material uno a uno sobre glassEffect funciona bien hasta la primera pantalla en la que dos elementos glass — o un elemento glass y un componente del sistema — acaban uno junto al otro. A partir de ahí, la pregunta deja de ser de sintaxis de API y pasa a ser qué capa de tu app es funcional y cuál es contenido — y esa distinción, más que nada, decide si la adaptación termina pareciendo nativa.
Enlaces útiles:



