Cómo cambiar la forma de los botones Liquid Glass en SwiftUI
Arturo Rivas Arias
Liquid Glass ha cambiado notablemente la apariencia de los controles del sistema en las versiones más recientes de las plataformas de Apple. En SwiftUI, una de las formas más sencillas de adoptar este lenguaje visual consiste en aplicar el estilo .glass a un botón.
Button("Iniciar grabación", systemImage: "record.circle") {
startRecording()
}
.buttonStyle(.glass)
El resultado mantiene el comportamiento nativo del botón y añade la superficie translúcida, los reflejos y las respuestas visuales propias de Liquid Glass. Sin embargo, la forma elegida automáticamente por SwiftUI no siempre encaja con el contenido del control.
Un botón de texto suele funcionar bien con el contorno de cápsula, mientras que una acción representada únicamente mediante un icono puede resultar más natural dentro de un círculo. Para resolverlo no es necesario crear un ButtonStyle personalizado ni recortar manualmente el resultado. SwiftUI ya proporciona el modificador buttonBorderShape(_:).
Cambiar la forma con buttonBorderShape(_:)
El modificador buttonBorderShape(_:) indica a SwiftUI qué forma debe utilizar el estilo del botón para dibujar su superficie exterior.
Button {
toggleMicrophone()
} label: {
Image(systemName: "mic.fill")
}
.buttonBorderShape(.circle)
.buttonStyle(.glass)
En este caso, .buttonStyle(.glass) aplica la apariencia Liquid Glass, mientras que .buttonBorderShape(.circle) solicita que la superficie del control sea circular.
La diferencia parece pequeña, pero resulta importante: la forma no se aplica después de representar el botón, sino que participa en la construcción del estilo. SwiftUI puede adaptar correctamente el material, los reflejos, el resaltado al pulsar, el foco, el puntero y cualquier otro comportamiento específico de la plataforma.
Formas disponibles
ButtonBorderShape ofrece varias formas definidas por el sistema:
.buttonBorderShape(.automatic)
.buttonBorderShape(.capsule)
.buttonBorderShape(.circle)
.buttonBorderShape(.roundedRectangle)
.buttonBorderShape(.roundedRectangle(radius: 14))
automatic
Permite que SwiftUI decida la forma adecuada según la plataforma, el estilo y el contexto en el que aparece el botón.
Button("Sincronizar") {
synchronizeData()
}
.buttonBorderShape(.automatic)
.buttonStyle(.glass)
Es la opción más segura cuando el botón debe integrarse con las convenciones del sistema y no existe una necesidad concreta de fijar el contorno.
capsule
La cápsula suele funcionar especialmente bien con botones que contienen texto, porque puede crecer horizontalmente sin alterar la curvatura de los extremos.
Button("Añadir participante", systemImage: "person.badge.plus") {
addParticipant()
}
.buttonBorderShape(.capsule)
.buttonStyle(.glass)
También es una buena elección para acciones principales, filtros o controles compactos cuyo título puede variar de longitud.
circle
La forma circular encaja con botones que contienen un único símbolo y representan acciones rápidas.
Button {
capturePhoto()
} label: {
Image(systemName: "camera.fill")
.font(.title3)
}
.buttonBorderShape(.circle)
.buttonStyle(.glass)
Conviene asegurarse de que el contenido dispone de espacio suficiente y de que el área interactiva no queda demasiado reducida. La forma visual puede ser circular, pero el botón debe seguir siendo cómodo de pulsar.
roundedRectangle
El rectángulo redondeado resulta útil cuando se necesita una geometría más contenida que la cápsula, especialmente en controles con varias líneas, iconos de mayor tamaño o composiciones más cercanas a una tarjeta.
Button {
openScanner()
} label: {
Label("Escanear documento", systemImage: "doc.viewfinder")
.frame(maxWidth: .infinity)
}
.buttonBorderShape(.roundedRectangle(radius: 18))
.buttonStyle(.glassProminent)
La variante con radio permite controlar la curvatura sin abandonar las guías de diseño nativas.
Aplicar la forma a varios botones
Al igual que otros modificadores de entorno, buttonBorderShape(_:) puede colocarse en un contenedor. Los botones descendientes heredarán la configuración.
HStack(spacing: 12) {
Button {
moveBackward()
} label: {
Image(systemName: "backward.fill")
}
Button {
togglePlayback()
} label: {
Image(systemName: "playpause.fill")
}
Button {
moveForward()
} label: {
Image(systemName: "forward.fill")
}
}
.buttonBorderShape(.circle)
.buttonStyle(.glass)
Esta técnica permite mantener una apariencia coherente en grupos de acciones relacionadas y evita repetir los mismos modificadores en cada botón.
También es posible sobrescribir la forma en un elemento concreto:
HStack(spacing: 12) {
Button {
showPreviousItem()
} label: {
Image(systemName: "chevron.left")
}
Button("Finalizar") {
finishSession()
}
.buttonBorderShape(.capsule)
}
.buttonBorderShape(.circle)
.buttonStyle(.glass)
El contenedor define una forma circular para el conjunto, pero el botón con texto utiliza una cápsula más adecuada para su contenido.
Por qué clipShape no es equivalente
Una posible solución sería aplicar clipShape después del estilo:
Button {
showLocation()
} label: {
Image(systemName: "location.fill")
}
.buttonStyle(.glass)
.clipShape(Circle())
Aunque el resultado puede parecer correcto a simple vista, ambos modificadores tienen responsabilidades diferentes.
clipShape recorta la vista una vez que ya ha sido representada. El estilo Liquid Glass ha calculado previamente su superficie, sus efectos y sus estados utilizando la forma que consideraba adecuada. El recorte únicamente elimina los píxeles que quedan fuera del círculo.
buttonBorderShape(_:), en cambio, proporciona la forma al sistema antes de construir el botón. Esto permite que el estilo adapte su apariencia y sus interacciones desde el principio.
Por este motivo, clipShape sigue siendo útil para recortar imágenes, fondos o vistas personalizadas, pero no debería ser la primera opción para cambiar la geometría de un botón con el estilo del sistema.
buttonBorderShape no modifica el contenido
El modificador controla la forma de la superficie del botón, pero no reorganiza automáticamente su etiqueta. Si una imagen, un texto o una composición interna ocupa demasiado espacio, será necesario ajustar también el contenido.
Button {
createBookmark()
} label: {
Image(systemName: "bookmark.fill")
.frame(width: 22, height: 22)
.padding(8)
}
.buttonBorderShape(.circle)
.buttonStyle(.glass)
El espaciado debe elegirse con cuidado. Añadir demasiado padding puede producir un botón visualmente desproporcionado, mientras que eliminarlo por completo puede reducir la superficie interactiva.
En general, conviene dejar que el propio estilo gestione el tamaño y utilizar ajustes manuales únicamente cuando el diseño lo necesite.
Combinar la forma con glassProminent
SwiftUI también ofrece el estilo .glassProminent, pensado para acciones que necesitan destacar frente a otros controles Liquid Glass.
Button("Guardar cambios", systemImage: "checkmark") {
saveChanges()
}
.buttonBorderShape(.capsule)
.buttonStyle(.glassProminent)
La forma y la prominencia son decisiones independientes. buttonBorderShape(_:) define la geometría, mientras que buttonStyle(_:) determina cómo se representa y cómo responde el botón.
Esta separación permite construir jerarquías visuales coherentes sin recurrir a estilos completamente personalizados.
Adaptar la forma al tipo de acción
La forma de un botón no debería elegirse únicamente por motivos estéticos. También comunica cómo debe interpretarse el control.
Los botones circulares suelen asociarse con acciones compactas e independientes, especialmente cuando contienen símbolos reconocibles. Las cápsulas funcionan mejor con títulos breves y acciones principales. Los rectángulos redondeados ofrecen más espacio para etiquetas complejas o controles que necesitan una presencia visual mayor.
Mantener estas convenciones facilita que el usuario comprenda la interfaz antes incluso de leer todos sus elementos.
VStack(spacing: 16) {
HStack {
Button {
decreaseZoom()
} label: {
Image(systemName: "minus.magnifyingglass")
}
Button {
increaseZoom()
} label: {
Image(systemName: "plus.magnifyingglass")
}
}
.buttonBorderShape(.circle)
.buttonStyle(.glass)
Button("Ajustar a la pantalla", systemImage: "arrow.up.left.and.arrow.down.right") {
fitToScreen()
}
.buttonBorderShape(.capsule)
.buttonStyle(.glassProminent)
}
El grupo superior utiliza círculos porque contiene acciones simétricas basadas en iconos. El botón inferior emplea una cápsula porque incluye texto y representa una acción más explícita.
Compatibilidad entre plataformas
buttonBorderShape(_:) forma parte de SwiftUI desde antes de Liquid Glass y también puede utilizarse con estilos como .bordered y .borderedProminent. Su comportamiento concreto depende del contexto y de la plataforma.
Button("Crear proyecto") {
createProject()
}
.buttonBorderShape(.roundedRectangle(radius: 12))
.buttonStyle(.borderedProminent)
Esta reutilización permite mantener una misma intención mediante la geometría al adaptar una aplicación a distintas versiones del sistema. En las versiones recientes de las plataformas puede combinarse con los estilos de cristal, mientras que en versiones anteriores puede conservarse junto a los estilos de borde tradicionales mediante comprobaciones de disponibilidad.
struct AdaptiveActionButton: View {
let action: () -> Void
var body: some View {
Button("Exportar", systemImage: "square.and.arrow.up", action: action)
.buttonBorderShape(.capsule)
.modifier(AdaptiveGlassButtonStyle())
}
}
private struct AdaptiveGlassButtonStyle: ViewModifier {
@ViewBuilder
func body(content: Content) -> some View {
if #available(iOS 26, macOS 26, *) {
content.buttonStyle(.glass)
} else {
content.buttonStyle(.bordered)
}
}
}
De esta forma, la estructura del botón permanece estable y solo cambia el estilo disponible en cada versión.
Conclusión
Los estilos .glass y .glassProminent permiten adoptar Liquid Glass sin reconstruir manualmente cada control. Cuando la forma automática no encaja con el contenido, buttonBorderShape(_:) ofrece el mecanismo adecuado para modificarla.
La principal ventaja frente a clipShape es que no recorta un resultado ya determinado. La silueta participa directamente en la creación del botón y permite que SwiftUI conserve los efectos, las interacciones y las adaptaciones propias de cada plataforma.
Para botones basados únicamente en iconos, .circle suele ser la opción más natural. Para acciones con texto, .capsule ofrece una geometría flexible. Cuando se necesita más control, .roundedRectangle(radius:) permite ajustar la silueta sin abandonar el estilo nativo del sistema.