Pour révéler une barre de progression SwiftUI avec dégradé sans comprimer ses couleurs, conservez au dégradé la largeur de toute la piste et animez un masque aligné sur le bord de départ. Cet exemple macOS met en œuvre ce comportement dans un ProgressViewStyle personnalisé.
La tâche consiste à créer un indicateur de progression redimensionnable pour une activité fictive sur ordinateur. À 25 % d’avancement, il doit révéler le premier quart d’une séquence de couleurs fixe. Les couleurs suivantes doivent apparaître au fur et à mesure de la progression, tandis que les couleurs déjà visibles restent en place.
Pourquoi les couleurs se compriment-elles lorsque la barre de progression raccourcit ?
Apple indique que LinearGradient fait correspondre ses points de départ et d’arrivée au rectangle englobant de la forme qu’il remplit. Par conséquent, attribuer au dégradé une largeur proportionnelle à la progression modifie aussi l’espace sur lequel ses couleurs sont réparties. Un dégradé d’un quart de la largeur contient toujours toute sa séquence de couleurs. LinearGradient — Documentation pour les développeurs Apple
Pour obtenir un effet de révélation, séparez deux dimensions : la largeur du dégradé correspond à celle de toute la piste, tandis que la largeur visible correspond à la largeur de la piste multipliée par la fraction achevée. Cette approche découle du comportement documenté du dégradé ; il ne s’agit pas d’une API distincte pour les barres de progression.
Prenons une piste de 320 points à titre d’exemple, avec des couleurs bleue, violette et orange aux positions normalisées 0, 0,5 et 1. Le milieu violet reste à 160 points pendant toute la révélation :
| Progression | Largeur totale du dégradé | Largeur du masque | Résultat visible |
|---|---|---|---|
| 0 % | 320 points | 0 point | Piste seule |
| 25 % | 320 points | 80 points | Premier quart du dégradé |
| 50 % | 320 points | 160 points | Dégradé jusqu’à son milieu violet |
| 100 % | 320 points | 320 points | Séquence complète de couleurs |
Cela vous donne un repère utile pour le diagnostic : à 25 %, le point orange à l’extrémité ne doit pas encore être visible. S’il apparaît déjà au bord de progression, vérifiez si la largeur dépendant de la progression est appliquée au dégradé.
Créer une démo complète de barre de progression avec dégradé pour macOS
Utilisez une cible d’app SwiftUI pour macOS avec une cible de déploiement macOS 12 ou ultérieure. La surcharge de mask(alignment:_:) qui prend en compte l’alignement et qui est utilisée ci-dessous est disponible à partir de macOS 12. Apple explique qu’elle applique le canal alpha d’une autre vue à la vue masquée. mask(alignment:_:) — Documentation pour les développeurs Apple
Remplacez le fichier du point d’entrée de l’app généré par ce programme complet. Vous pouvez laisser inutilisé le fichier de vue de contenu généré. La cible doit contenir exactement une déclaration @main, conformément au contrat d’Apple sur le point d’entrée d’une app. App — Documentation pour les développeurs 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
}
}
}
Le style personnalisé implémente makeBody(configuration:), le point d’extension documenté pour définir l’apparence d’une vue de progression. Il affiche les deux libellés fournis et lit la fraction d’achèvement dans la configuration. En conservant la personnalisation dans ce protocole, l’interface ProgressView utilisée par l’appelant reste intacte. ProgressViewStyle — Documentation pour les développeurs Apple
L’exemple démarre à 25 %. Utilisez les boutons pour accéder à des étapes de contrôle reproductibles, faites glisser le curseur pour examiner des valeurs intermédiaires et redimensionnez la fenêtre pour vérifier la mise en page. Ces contrôles modifient un état local fictif ; ils ne représentent pas un travail mesuré. Le code source a été examiné et les calculs de largeur ont été vérifiés avec Python, mais le programme SwiftUI n’a pas été compilé ni exécuté dans l’environnement de recherche.
Comment la mise en page stabilise le dégradé
GeometryReader mesure l’espace attribué à l’indicateur. Apple le décrit comme un conteneur dont le contenu dépend de sa propre taille et de son espace de coordonnées, et dont la taille préférée est flexible. Appliquer .frame(height: 14) à l’extérieur lui donne une hauteur explicite au sein de la pile verticale, alors que ce conteneur est flexible. GeometryReader — Documentation pour les développeurs Apple
Dans ce conteneur, le dégradé reçoit toute la largeur mesurée. Seul le rectangle à l’intérieur du masque reçoit la largeur multipliée. Lorsque la progression change, le cadre du dégradé reste constant pour une taille de fenêtre donnée.
L’alignement .leading du masque est essentiel : il place le masque plus petit contre le bord de départ du dégradé. Le paramètre d’alignement d’un cadre positionne le contenu à l’intérieur de ce cadre ; il ne remplace pas l’alignement entre le masque et la vue masquée. Apple décrit ces deux responsabilités d’alignement séparément. mask(alignment:_:) — Documentation pour les développeurs Apple, frame(width:height:alignment:) — Documentation pour les développeurs Apple
Le rectangle opaque révèle le dégradé partout où il est présent. Sa teinte ne détermine pas les couleurs révélées, car le masque utilise le canal alpha. Le remplacer par un rectangle transparent masquerait le dégradé. Placez la piste grise à l’extérieur du masque pour qu’elle reste visible lorsque la progression est nulle.
La capsule finale découpe la piste et le dégradé révélé selon le même contour extérieur. Apple indique que le découpage conserve les parties couvertes par la forme de découpage. Dans cet exemple, le bord de progression reste droit jusqu’à ce qu’il atteigne l’extrémité arrondie de la capsule ; un masque de révélation en forme de capsule arrondirait le bord de progression. clipShape(_:style:) — Documentation pour les développeurs Apple
Le redimensionnement de la fenêtre modifie la largeur totale de la piste, et le dégradé est donc recalculé sur cette nouvelle largeur. Ici, « couleurs stables » signifie que leurs positions par rapport à la piste restent constantes lors des mises à jour de progression, et non qu’elles conservent des coordonnées fixes à l’écran lorsque la fenêtre est redimensionnée.
Préserver les valeurs de progression, les types et le comportement du cycle de vie
Le style reçoit fractionCompleted sous la forme d’un Double facultatif. Apple définit sa plage numérique de 0 à 1 et utilise nil pour une progression indéterminée ou relative à un intervalle de dates. Convertir cette fraction en CGFloat lors du calcul de la largeur rend explicite la conversion entre types numériques. fractionCompleted — Documentation pour les développeurs Apple
L’exemple borne les fractions finies par précaution et renvoie vers un indicateur standard indéterminé lorsque la fraction est absente ou non finie. Ce traitement des valeurs non finies est un choix explicite de cet exemple. Il évite d’interpréter une fraction indisponible comme une progression nulle. L’indicateur interne sélectionne explicitement .circular pour utiliser un style intégré.
Au point d’appel, fournissez une valeur achevée finie et un total positif fini. L’initialiseur basé sur une valeur accepte nil pour une progression indéterminée et indique que les valeurs hors plage sont ramenées à ses limites. L’exemple utilise un total de 1 et limite le curseur au même intervalle. init(value:total:label:currentValueLabel:) — Documentation pour les développeurs Apple
La vue parente possède l’état de progression, tandis que le style tire son rendu de la configuration. SwiftUI met à jour les vues dépendantes lorsque l’état change. Cet exemple n’a donc besoin ni d’un minuteur, ni d’un rappel d’apparition, ni d’une tâche en arrière-plan pour conserver son état visuel. State — Documentation pour les développeurs Apple
L’animation s’applique au rectangle de révélation. Le modificateur lié à une valeur applique une animation lorsque la fraction surveillée change ; la durée d’un quart de seconde est un choix de présentation donné à titre d’exemple. Elle ne calcule pas la progression de la tâche. Supprimez le modificateur si des mises à jour immédiates conviennent mieux à votre interface. animation(_:value:) — Documentation pour les développeurs Apple
Il existe une limite documentée pour la progression pilotée par un minuteur : Apple indique que les vues de progression relatives à une date ne prennent pas en charge les styles personnalisés. Pour cet aspect avec dégradé, utilisez l’initialiseur numérique de progression et demandez au propriétaire de l’activité de fournir sa fraction achevée. Si vous avez besoin des mises à jour automatiques relatives à une date proposées par Apple, utilisez cet initialiseur avec la présentation qu’il prend en charge. init(timerInterval:countsDown:label:currentValueLabel:) — Documentation pour les développeurs Apple
Vérifier la révélation à l’aide d’une courte séquence de diagnostic
Effectuez ces vérifications après avoir créé la cible macOS. Elles permettent de distinguer les problèmes de répartition des couleurs de ceux d’alignement et de dimensionnement :
- Sélectionnez
Reset. La piste grise doit rester visible, sans dégradé. - Sélectionnez
25%. Seules les premières couleurs du dégradé doivent apparaître. Si l’extrémité orange est visible, cela suggère que le dégradé lui-même est raccourci. - Sélectionnez
50%. Avec les points de couleur fournis, la limite de révélation doit atteindre le milieu violet. - Sélectionnez
Complete. Le dégradé doit remplir la piste jusqu’à l’orange. - Redimensionnez la fenêtre à 50 %. La partie visible doit rester égale à la moitié de la nouvelle taille de la piste.
- Activez
Indeterminate. Le remplissage numérique doit céder la place à l’indicateur intégré, tandis que les libellés de tâche et de valeur actuelle restent visibles.
Si la révélation s’étend depuis le centre, vérifiez .mask(alignment: .leading). Si l’indicateur occupe trop d’espace vertical, vérifiez le cadre placé à l’extérieur de GeometryReader. Si la modification de la progression déplace tous les points de couleur, vérifiez que seul le masque utilise le multiplicateur d’achèvement.
La règle réutilisable est simple : dimensionnez le dégradé pour toute la piste, puis utilisez un masque de taille distincte pour révéler la progression. Le fait de séparer ces responsabilités facilite l’analyse de la progression partielle, du redimensionnement et de l’animation, tout en préservant la configuration documentée de la vue de progression.
Sources
- 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