Para mostrar una barra de progreso con gradiente en SwiftUI sin comprimir sus colores, mantén el gradiente tan ancho como la pista completa y anima una máscara alineada al borde inicial. Este ejemplo para macOS implementa ese comportamiento en un ProgressViewStyle personalizado.
La tarea consiste en crear un indicador de progreso redimensionable para escritorio para una actividad simulada. Al llegar al 25 % de progreso, debe mostrar el primer cuarto de una secuencia de colores fija. Los colores posteriores deben aparecer a medida que avance el progreso, mientras los colores existentes permanecen en su sitio.
¿Por qué se comprimen los colores cuando la barra de progreso se acorta?
Apple documenta que LinearGradient asigna sus puntos inicial y final al rectángulo delimitador de la forma que rellena. Por consiguiente, dar al gradiente un ancho proporcional al progreso también cambia el espacio en el que se distribuyen sus colores. Un gradiente de un cuarto de ancho sigue conteniendo toda la secuencia de colores. LinearGradient — Documentación para desarrolladores de Apple
Para lograr un efecto de revelado, separa dos dimensiones: el ancho del gradiente es el ancho total de la pista, mientras que el ancho visible es el ancho de la pista multiplicado por la fracción completada. Este diseño se deriva del comportamiento documentado del gradiente; no es una API independiente para barras de progreso.
Considera una pista ilustrativa de 320 puntos con paradas de color azul, morado y naranja en las posiciones normalizadas 0, 0,5 y 1. El punto medio morado permanece en los 160 puntos durante todo el revelado:
| Progreso | Ancho total del gradiente | Ancho de la máscara | Resultado visible |
|---|---|---|---|
| 0 % | 320 puntos | 0 puntos | Solo la pista |
| 25 % | 320 puntos | 80 puntos | Primer cuarto del gradiente |
| 50 % | 320 puntos | 160 puntos | Gradiente hasta el punto medio morado |
| 100 % | 320 puntos | 320 puntos | Secuencia de colores completa |
Esto te ofrece una comprobación útil: al 25 %, la parada naranja del extremo final debe permanecer oculta. Si ya aparece en el borde que avanza, comprueba si se está aplicando al gradiente el ancho que depende del progreso.
Crea una demostración completa de una barra de progreso con gradiente para macOS
Usa un destino de app SwiftUI para macOS con un destino de implementación de macOS 12 o posterior. La sobrecarga de mask(alignment:_:) que tiene en cuenta la alineación y que se usa a continuación está disponible desde macOS 12. Apple explica que aplica el canal alfa de otra vista a la vista enmascarada. mask(alignment:_:) — Documentación para desarrolladores de Apple
Reemplaza el archivo generado del punto de entrada de la app por este programa completo. El archivo generado para la vista de contenido puede quedar sin usar. Mantén una sola declaración @main en el destino, tal como exige el contrato del punto de entrada de las apps de Apple. App — Documentación para desarrolladores de Apple
import SwiftUI
@main
struct GradientRevealDemoApp: App {
var body: some Scene {
WindowGroup {
GradientRevealDemo()
}
}
}
struct GradientRevealDemo: View {
@State private var progress = 0.25
@State private var isIndeterminate = false
var body: some View {
VStack(alignment: .leading, spacing: 20) {
ProgressView(
value: isIndeterminate ? nil : progress,
total: 1.0
) {
Text("Sample activity")
} currentValueLabel: {
if isIndeterminate {
Text("In progress")
} else {
Text("\(Int((progress * 100).rounded()))%")
}
}
.progressViewStyle(GradientRevealStyle())
Slider(value: $progress, in: 0...1, step: 0.01) {
Text("Sample completion")
}
.disabled(isIndeterminate)
HStack {
Button("Reset") { progress = 0 }
Button("25%") { progress = 0.25 }
Button("50%") { progress = 0.5 }
Button("Complete") { progress = 1 }
}
.disabled(isIndeterminate)
Toggle("Indeterminate", isOn: $isIndeterminate)
}
.padding(24)
.frame(minWidth: 360, idealWidth: 520, minHeight: 240)
}
}
@MainActor
struct GradientRevealStyle: ProgressViewStyle {
func makeBody(configuration: Configuration) -> some View {
let fraction = configuration.fractionCompleted.flatMap {
$0.isFinite ? min(max($0, 0), 1) : nil
}
VStack(alignment: .leading, spacing: 8) {
configuration.label
if let fraction = fraction {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.fill(Color.gray.opacity(0.2))
LinearGradient(
stops: [
.init(color: .blue, location: 0),
.init(color: .purple, location: 0.5),
.init(color: .orange, location: 1)
],
startPoint: .leading,
endPoint: .trailing
)
.frame(width: geometry.size.width, height: 14)
.mask(alignment: .leading) {
Rectangle()
.frame(
width: geometry.size.width
* CGFloat(fraction),
height: 14
)
.animation(
.easeInOut(duration: 0.25),
value: fraction
)
}
}
.frame(width: geometry.size.width, height: 14)
.clipShape(Capsule())
}
.frame(height: 14)
} else {
ProgressView()
.progressViewStyle(.circular)
}
configuration.currentValueLabel
}
}
}
El estilo personalizado implementa makeBody(configuration:), el punto de extensión documentado para la apariencia de una vista de progreso. Muestra ambas etiquetas proporcionadas y lee la fracción completada de la configuración. Mantener los estilos en este protocolo preserva la interfaz ProgressView del código que lo llama. ProgressViewStyle — Documentación para desarrolladores de Apple
El ejemplo empieza al 25 %. Usa los botones para repetir las comprobaciones, arrastra el deslizador para inspeccionar valores intermedios y cambia el tamaño de la ventana para revisar el diseño. Estos controles modifican el estado local simulado; no representan trabajo medido. Se inspeccionó el código fuente y se comprobaron los cálculos de ancho con Python, pero el programa SwiftUI no se compiló ni se ejecutó en el entorno de investigación.
Cómo el diseño mantiene estable el gradiente
GeometryReader mide el espacio asignado al indicador. Apple lo describe como un contenedor cuyo contenido depende de su propio tamaño y espacio de coordenadas, y que tiene un tamaño preferido flexible. Aplicar .frame(height: 14) fuera de él asigna a este contenedor, que de otro modo sería flexible, una altura explícita dentro de la pila vertical. GeometryReader — Documentación para desarrolladores de Apple
Dentro de ese contenedor, el gradiente recibe el ancho total medido. Solo el rectángulo dentro de la máscara recibe el ancho multiplicado. Cuando cambia el progreso, el marco del gradiente permanece constante para un tamaño de ventana determinado.
La alineación .leading de la máscara es esencial: coloca la máscara más pequeña contra el borde inicial del gradiente. El parámetro de alineación de un marco posiciona el contenido dentro de ese marco; no reemplaza la alineación entre la máscara y la vista enmascarada. Apple documenta que son responsabilidades de alineación independientes. mask(alignment:_:) — Documentación para desarrolladores de Apple, frame(width:height:alignment:) — Documentación para desarrolladores de Apple
El rectángulo opaco muestra el gradiente dondequiera que esté presente. Su tono no determina qué colores se revelan, porque el enmascaramiento usa el canal alfa. Si lo sustituyes por un rectángulo transparente, ocultarás el gradiente. Mantén la pista gris fuera de la máscara para que siga visible cuando el progreso sea cero.
La cápsula final recorta la pista combinada y el gradiente revelado con la misma silueta exterior. Apple documenta que el recorte conserva las partes cubiertas por la forma de recorte. En este ejemplo, el borde que avanza es recto hasta que llega al extremo redondeado de la cápsula; una máscara de revelado con forma de cápsula produciría un borde redondeado. clipShape(_:style:) — Documentación para desarrolladores de Apple
Al cambiar el tamaño de la ventana, cambia el ancho total de la pista, así que el gradiente se recalcula para ese nuevo ancho. Aquí, «colores estables» significa que mantienen posiciones estables respecto a la pista durante las actualizaciones del progreso, no que conserven coordenadas fijas en la pantalla al cambiar el tamaño de la ventana.
Conserva los valores de progreso, los tipos y el comportamiento del ciclo de vida
El estilo recibe fractionCompleted como un Double opcional. Apple define su rango numérico entre 0 y 1, y usa nil para indicar progreso indeterminado o progreso relativo a un intervalo de fechas. Convertir esa fracción a CGFloat al calcular el ancho hace explícito el límite entre tipos numéricos. fractionCompleted — Documentación para desarrolladores de Apple
El ejemplo limita las fracciones finitas por precaución y dirige las fracciones ausentes o no finitas a un indicador indeterminado estándar. El manejo de valores no finitos es una decisión explícita de este ejemplo. Evita interpretar una fracción no disponible como progreso cero. El indicador interno selecciona .circular explícitamente para usar un estilo integrado.
En el punto de llamada, proporciona una cantidad completada finita y un total finito y positivo. El inicializador basado en valores acepta nil para el progreso indeterminado y documenta que los valores fuera del rango se ajustan a sus límites. El ejemplo usa un total de 1 y limita el deslizador al mismo intervalo. init(value:total:label:currentValueLabel:) — Documentación para desarrolladores de Apple
La vista principal es propietaria del estado de progreso, mientras que el estilo deriva su representación de la configuración. SwiftUI actualiza las vistas dependientes cuando cambia el estado. Por tanto, este ejemplo no necesita temporizador, llamada de ciclo de vida ni tarea en segundo plano para mantener su estado visual. State — Documentación para desarrolladores de Apple
La animación corresponde al rectángulo de revelado. El modificador asociado al valor aplica una animación cuando cambia la fracción observada; la duración elegida de un cuarto de segundo es una decisión ilustrativa de presentación. No calcula el progreso de la tarea. Quita el modificador si las actualizaciones inmediatas se adaptan mejor a tu interfaz. animation(_:value:) — Documentación para desarrolladores de Apple
Hay una limitación documentada para el progreso controlado por temporizador: Apple indica que las vistas de progreso relativas a fechas no admiten estilos personalizados. Para esta apariencia con gradiente, usa el inicializador de progreso numérico y haz que el responsable de la actividad proporcione su fracción completada. Si necesitas las actualizaciones automáticas relativas a fechas de Apple, usa ese inicializador con su presentación compatible. init(timerInterval:countsDown:label:currentValueLabel:) — Documentación para desarrolladores de Apple
Comprueba el revelado con una breve secuencia de diagnóstico
Usa estas comprobaciones después de compilar el destino para macOS. Permiten distinguir los problemas de asignación de colores de los problemas de alineación y tamaño:
- Selecciona
Reset. La pista gris debe permanecer visible, sin ningún gradiente. - Selecciona
25%. Solo deben aparecer los primeros colores del gradiente. Si el extremo naranja está visible, es posible que se esté acortando el propio gradiente. - Selecciona
50%. Con las paradas proporcionadas, el límite del revelado debe llegar al punto medio morado. - Selecciona
Complete. El gradiente debe llenar la pista hasta el naranja. - Cambia el tamaño de la ventana al 50 %. La parte visible debe seguir ocupando la mitad de la pista, ahora redimensionada.
- Activa
Indeterminate. El relleno numérico debe dar paso al indicador integrado, mientras las etiquetas de la tarea y del valor actual permanecen visibles.
Si el revelado crece desde el centro hacia fuera, comprueba .mask(alignment: .leading). Si el indicador ocupa demasiado espacio vertical, comprueba el marco fuera de GeometryReader. Si al cambiar el progreso se mueve cada parada de color, comprueba que solo la máscara use el multiplicador de progreso completado.
La regla reutilizable es sencilla: ajusta el tamaño del gradiente al ancho total de la pista y luego usa una máscara de tamaño independiente para revelar el progreso. Mantener separadas esas responsabilidades facilita razonar sobre el progreso parcial, el redimensionamiento y la animación, al tiempo que se conserva la configuración documentada de la vista de progreso.
Fuentes
- LinearGradient — Apple
- mask(alignment:_:) — Apple
- App — Apple
- ProgressViewStyle — Apple
- GeometryReader — Apple
- frame(width:height:alignment:) — Apple
- clipShape(_:style:) — Apple
- fractionCompleted — Apple
- init(value:total:label:currentValueLabel:) — Apple
- State — Apple
- animation(_:value:) — Apple
- init(timerInterval:countsDown:label:currentValueLabel:) — Apple