Per mostrare una barra di avanzamento SwiftUI con gradiente senza comprimere i colori, mantieni il gradiente largo quanto l’intera traccia e anima una maschera allineata a sinistra. Questo esempio per macOS implementa questo comportamento in un ProgressViewStyle personalizzato.

L’obiettivo è creare un indicatore di avanzamento desktop ridimensionabile per un’attività simulata. Al 25% di completamento, dovrebbe mostrare il primo quarto di una sequenza di colori fissa. I colori successivi dovrebbero apparire man mano che l’avanzamento procede, mentre quelli già visibili restano al loro posto.

Perché i colori si comprimono quando la barra di avanzamento diventa più corta?

Apple documenta che LinearGradient mappa i punti iniziale e finale nel rettangolo di delimitazione della forma che riempie. Di conseguenza, assegnare al gradiente una larghezza proporzionale all’avanzamento cambia anche lo spazio su cui vengono distribuiti i colori. Anche un gradiente largo un quarto contiene l’intera sequenza di colori. LinearGradient — Documentazione per sviluppatori Apple

Per ottenere un effetto di rivelazione, separa due dimensioni: la larghezza del gradiente corrisponde all’intera larghezza della traccia, mentre la larghezza visibile corrisponde alla larghezza della traccia moltiplicata per la frazione completata. Si tratta di una soluzione progettuale derivata dal comportamento documentato del gradiente, non di un’API specifica per le barre di avanzamento.

Considera una traccia illustrativa di 320 punti, con interruzioni blu, viola e arancione nelle posizioni normalizzate 0, 0,5 e 1. Il punto medio viola resta a 160 punti per tutta la rivelazione:

Avanzamento Larghezza del gradiente completo Larghezza della maschera Risultato visibile
0% 320 punti 0 punti Solo la traccia
25% 320 punti 80 punti Primo quarto del gradiente
50% 320 punti 160 punti Gradiente fino al punto medio viola
100% 320 punti 320 punti Intera sequenza di colori

Questo offre un utile criterio diagnostico: al 25%, l’interruzione arancione finale dovrebbe restare nascosta. Se compare già sul bordo di avanzamento, verifica se alla larghezza dipendente dall’avanzamento viene applicata la larghezza del gradiente.

Crea una demo completa di barra di avanzamento con gradiente per macOS

Usa un target app SwiftUI per macOS con una versione minima di macOS 12 o successiva. L’overload di mask(alignment:_:) che accetta l’allineamento, usato qui sotto, è disponibile da macOS 12. Apple lo descrive come un modo per applicare al contenuto mascherato il canale alfa di un’altra vista. mask(alignment:_:) — Documentazione per sviluppatori Apple

Sostituisci il file del punto di ingresso generato per l’app con questo programma completo. Il file generato per la vista del contenuto può restare inutilizzato. Nel target deve esserci una sola dichiarazione @main, come richiesto dal contratto di Apple per il punto di ingresso delle app. App — Documentazione per sviluppatori 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
        }
    }
}

Lo stile personalizzato implementa makeBody(configuration:), il punto di estensione documentato per l’aspetto di una vista di avanzamento. Mostra entrambe le etichette fornite e legge la frazione di completamento dalla configurazione. Mantenere la personalizzazione in questo protocollo preserva l’interfaccia ProgressView di chi chiama. ProgressViewStyle — Documentazione per sviluppatori Apple

L’esempio inizia al 25%. Usa i pulsanti per passare a valori di riferimento ripetibili, trascina il cursore per esaminare i valori intermedi e ridimensiona la finestra per controllare il layout. Questi controlli modificano uno stato locale simulato; non rappresentano un’attività misurata. Il codice sorgente è stato esaminato e i calcoli delle larghezze sono stati verificati con Python, ma il programma SwiftUI non è stato compilato né eseguito nell’ambiente di ricerca.

Come il layout mantiene stabile il gradiente

GeometryReader misura lo spazio assegnato all’indicatore. Apple lo descrive come un contenitore il cui contenuto dipende dalle sue dimensioni e dal suo spazio di coordinate, e che ha una dimensione preferita flessibile. Applicare .frame(height: 14) all’esterno gli assegna un’altezza esplicita all’interno dello stack verticale. GeometryReader — Documentazione per sviluppatori Apple

All’interno del contenitore, il gradiente riceve l’intera larghezza misurata. Solo il rettangolo all’interno della maschera riceve la larghezza moltiplicata. Quando cambia l’avanzamento, il frame del gradiente resta costante per una data dimensione della finestra.

L’allineamento .leading della maschera è fondamentale: posiziona la maschera più piccola contro il bordo iniziale del gradiente. Il parametro di allineamento di un frame posiziona il contenuto al suo interno; non sostituisce l’allineamento tra la maschera e la vista mascherata. Apple documenta queste responsabilità di allineamento come distinte. mask(alignment:_:) — Documentazione per sviluppatori Apple, frame(width:height:alignment:) — Documentazione per sviluppatori Apple

Il rettangolo opaco rivela il gradiente nei punti in cui è presente. La sua tonalità non determina i colori rivelati, perché la mascheratura usa il canale alfa. Sostituirlo con un rettangolo trasparente nasconderebbe il gradiente. Mantieni la traccia grigia all’esterno della maschera affinché resti visibile quando l’avanzamento è zero.

La capsula finale ritaglia la traccia e il gradiente rivelato combinati, adattandoli alla stessa sagoma esterna. Apple documenta il ritaglio come il mantenimento delle porzioni coperte dalla forma di ritaglio. Questo esempio mantiene intenzionalmente diritto il bordo di avanzamento finché non raggiunge l’estremità arrotondata della capsula; una maschera di rivelazione a forma di capsula produrrebbe invece un bordo di avanzamento arrotondato. clipShape(_:style:) — Documentazione per sviluppatori Apple

Ridimensionando la finestra cambia la larghezza completa della traccia, quindi il gradiente viene ricalcolato su quella nuova larghezza. Qui, “colori stabili” significa posizioni stabili rispetto alla traccia durante gli aggiornamenti dell’avanzamento, non coordinate fisse sullo schermo durante il ridimensionamento della finestra.

Mantieni i valori di avanzamento, i tipi e il comportamento del ciclo di vita

Lo stile riceve fractionCompleted come Double facoltativo. Apple ne definisce l’intervallo numerico da 0 a 1 e usa nil per l’avanzamento indeterminato o relativo a un intervallo di date. Convertendo la frazione in CGFloat nel calcolo della larghezza, il confine tra i tipi numerici è esplicito. fractionCompleted — Documentazione per sviluppatori Apple

Per precauzione, l’esempio limita le frazioni finite e indirizza quelle mancanti o non finite a un indicatore indeterminato standard. La gestione dei valori non finiti è una scelta esplicita di questo esempio. Evita di interpretare una frazione non disponibile come completamento pari a zero. L’indicatore interno seleziona esplicitamente .circular per usare uno stile integrato.

Nel punto in cui viene chiamato, fornisci una quantità completata finita e un totale finito e positivo. L’inizializzatore basato su un valore accetta nil per l’avanzamento indeterminato e documenta che i valori fuori intervallo vengono ricondotti ai limiti consentiti. L’esempio usa un totale pari a 1 e limita il cursore allo stesso intervallo. init(value:total:label:currentValueLabel:) — Documentazione per sviluppatori Apple

La vista genitore possiede lo stato di avanzamento, mentre lo stile ne ricava la rappresentazione dalla configurazione. SwiftUI aggiorna le viste dipendenti quando cambia lo stato. Perciò, questo esempio non richiede timer, callback di apparizione o attività in background per mantenere lo stato visivo. State — Documentazione per sviluppatori Apple

L’animazione riguarda il rettangolo di rivelazione. Il modificatore associato a un valore applica un’animazione quando cambia la frazione monitorata; la durata di un quarto di secondo è una scelta illustrativa di presentazione. Non calcola l’avanzamento dell’attività. Rimuovi il modificatore se nella tua interfaccia sono preferibili aggiornamenti immediati. animation(_:value:) — Documentazione per sviluppatori Apple

Per l’avanzamento gestito da timer c’è un limite documentato: Apple afferma che le viste di avanzamento relative alle date non supportano stili personalizzati. Per questo aspetto con gradiente, usa l’inizializzatore per l’avanzamento numerico e fai in modo che il proprietario dell’attività fornisca la frazione completata. Se ti servono gli aggiornamenti automatici relativi alle date di Apple, usa l’inizializzatore corrispondente con la presentazione supportata. init(timerInterval:countsDown:label:currentValueLabel:) — Documentazione per sviluppatori Apple

Verifica la rivelazione con una breve sequenza diagnostica

Dopo aver creato il target macOS, esegui questi controlli. Aiutano a distinguere i problemi di mappatura dei colori da quelli di allineamento e dimensionamento:

  1. Seleziona Reset. La traccia grigia dovrebbe restare visibile senza mostrare il gradiente.
  2. Seleziona 25%. Dovrebbero apparire solo i colori iniziali del gradiente. Se è visibile l’estremità arancione, probabilmente viene accorciato il gradiente stesso.
  3. Seleziona 50%. Con le interruzioni fornite, il limite della rivelazione dovrebbe raggiungere il punto medio viola.
  4. Seleziona Complete. Il gradiente dovrebbe riempire la traccia fino all’arancione.
  5. Ridimensiona la finestra al 50%. La porzione visibile dovrebbe restare pari a metà della traccia ridimensionata.
  6. Attiva Indeterminate. Il riempimento numerico dovrebbe lasciare il posto all’indicatore integrato, mentre le etichette dell’attività e del valore corrente restano presenti.

Se la rivelazione si espande dal centro verso l’esterno, controlla .mask(alignment: .leading). Se l’indicatore occupa troppo spazio in verticale, controlla il frame esterno a GeometryReader. Se cambiando l’avanzamento si spostano tutte le interruzioni di colore, verifica che il moltiplicatore di completamento venga usato solo dalla maschera.

La regola riutilizzabile è semplice: dimensiona il gradiente sull’intera traccia, poi usa una maschera di dimensioni separate per rivelare l’avanzamento. Mantenere distinte queste responsabilità semplifica la gestione del completamento parziale, del ridimensionamento e dell’animazione, preservando la configurazione documentata della vista di avanzamento.

Fonti