Om du vill visa en SwiftUI-förloppsindikator med gradient utan att komprimera färgerna låter du gradienten ha samma bredd som hela spåret och animerar en mask som är justerad mot början. Det här macOS-exemplet implementerar beteendet i en anpassad ProgressViewStyle.
Uppgiften är att skapa en storleksändringsbar förloppsindikator för ett syntetiskt förlopp i en skrivbordsapp. När förloppet nått 25 % ska den första fjärdedelen av en fast färgsekvens visas. Senare färger ska visas i takt med att förloppet fortskrider, medan befintliga färger ligger kvar på samma plats.
Varför komprimeras färgerna när förloppsindikatorn blir kortare?
Apple dokumenterar att LinearGradient mappar sina start- och slutpunkter till den omslutande rektangeln för formen den fyller. Om gradienten får en bredd som är proportionell mot förloppet ändras därför också utrymmet där färgerna fördelas. En gradient med en fjärdedels bredd innehåller fortfarande hela färgsekvensen. LinearGradient — Apple Developer Documentation
För en visningseffekt delar du upp två dimensioner: gradientens bredd är hela spårets bredd, medan den synliga bredden är spårets bredd multiplicerad med andelen som är klar. Det här är en lösning som bygger på gradientens dokumenterade beteende, inte ett separat API för förloppsindikatorer.
Tänk dig ett 320 punkter brett spår med blå, lila och orange färgstopp vid normaliserade positioner 0, 0.5 och 1. Den lila mittpunkten ligger kvar vid 160 punkter under hela visningen:
| Förlopp | Gradientens fulla bredd | Maskens bredd | Synligt resultat |
|---|---|---|---|
| 0 % | 320 punkter | 0 punkter | Endast spåret |
| 25 % | 320 punkter | 80 punkter | Gradientens första fjärdedel |
| 50 % | 320 punkter | 160 punkter | Gradient fram till dess lila mittpunkt |
| 100 % | 320 punkter | 320 punkter | Hela färgsekvensen |
Detta ger dig ett användbart felsökningstest: vid 25 % ska det orange färgstoppet längst bort fortfarande vara dolt. Om det redan syns vid den växande kanten bör du kontrollera om den progressberoende bredden tillämpas på gradienten.
Skapa en komplett macOS-demo med en gradientförloppsindikator
Använd ett macOS-appmål med SwiftUI och ett distributionsmål som är macOS 12 eller senare. Överlagringen av mask(alignment:_:) som tar hänsyn till justering och används nedan är tillgänglig från macOS 12. Apple beskriver den som att den applicerar alfakanalen från en annan vy på den maskerande vyn. mask(alignment:_:) — Apple Developer Documentation
Ersätt den genererade appens startpunktsfil med det här kompletta programmet. Den genererade filen för innehållsvyn kan lämnas oanvänd. Ha exakt en deklaration av @main i målet, enligt Apples kontrakt för appens startpunkt. App — Apple Developer Documentation
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
}
}
}
Den anpassade stilen implementerar makeBody(configuration:), den dokumenterade utökningspunkten för hur en förloppsvy visas. Den återger båda etiketter som skickas in och hämtar andelen som är klar från konfigurationen. När utseendet hanteras med det här protokollet bevaras anroparens ProgressView-gränssnitt. ProgressViewStyle — Apple Developer Documentation
Exemplet börjar vid 25 %. Använd knapparna för att återkomma till samma kontrollpunkter, dra i skjutreglaget för att granska mellanliggande värden och ändra fönstrets storlek för att granska layouten. Kontrollerna ändrar syntetiskt lokalt tillstånd; de representerar inte uppmätt arbete. Källkoden granskades och breddberäkningarna kontrollerades med Python, men SwiftUI-programmet kompilerades eller kördes inte i undersökningsmiljön.
Så håller layouten gradienten stabil
GeometryReader mäter utrymmet som tilldelats indikatorn. Apple beskriver den som en behållare vars innehåll beror på dess egen storlek och koordinatsystem, med en flexibel önskad storlek. Om du applicerar .frame(height: 14) utanför den får den här annars flexibla behållaren en bestämd höjd i den vertikala stacken. GeometryReader — Apple Developer Documentation
Inuti behållaren får gradienten hela den uppmätta bredden. Endast rektangeln inuti masken får den multiplicerade bredden. När förloppet ändras behåller gradientens ram samma storlek för ett givet fönster.
Maskens justering .leading är avgörande: den placerar den mindre masken mot gradientens början. Justeringsparametern för en ram placerar innehållet inuti ramen; den ersätter inte justeringen mellan masken och vyn som maskeras. Apple dokumenterar dessa som separata justeringsansvar. mask(alignment:_:) — Apple Developer Documentation, frame(width:height:alignment:) — Apple Developer Documentation
Den ogenomskinliga rektangeln visar gradienten där rektangeln finns. Dess nyans avgör inte vilka färger som visas eftersom maskeringen använder alfa. Om du ersätter den med en genomskinlig rektangel döljs gradienten. Låt det grå spåret ligga utanför masken så att det förblir synligt när förloppet är noll.
Den yttre kapseln klipper till sist det kombinerade spåret och den visade gradienten till samma yttre silhuett. Apple dokumenterar klippning som att de delar bevaras som täcks av klippformen. Exemplet behåller avsiktligt en rak växande kant tills den når kapselns rundade ände. En mask formad som en kapsel skulle ge den växande kanten en rundad form. clipShape(_:style:) — Apple Developer Documentation
När du ändrar fönstrets storlek ändras hela spårets bredd, så gradienten beräknas om över den nya bredden. Med ”stabila färger” avses här stabila positioner i förhållande till spåret när förloppet uppdateras, inte fasta skärmkoordinater när fönstrets storlek ändras.
Bevara förloppsvärden, typer och livscykelbeteende
Stilen får fractionCompleted som en valfri Double. Apple definierar dess numeriska intervall som 0 till 1 och använder nil för förlopp som inte kan bestämmas eller för förlopp i förhållande till ett datumintervall. Om du konverterar andelen till CGFloat i breddberäkningen blir den numeriska typgränsen tydlig. fractionCompleted — Apple Developer Documentation
Exemplet begränsar ändliga andelar som en försiktighetsåtgärd och visar en standardindikator för förlopp som inte kan bestämmas eller andelar som inte är ändliga. Hanteringen av icke-ändliga värden är en uttrycklig policy i exemplet. På så sätt tolkas inte en otillgänglig andel som noll procent klart. Den inre indikatorn väljer uttryckligen .circular för att använda en inbyggd stil.
Ange ett ändligt värde för hur mycket som är klart och en ändlig, positiv total vid anropsstället. Den värdebaserade initieraren godtar nil för förlopp som inte kan bestämmas och dokumenterar att värden utanför intervallet begränsas till dess gränser. Exemplet använder en total på 1 och begränsar skjutreglaget till samma intervall. init(value:total:label:currentValueLabel:) — Apple Developer Documentation
Den överordnade vyn äger förloppstillståndet, medan stilen härleder sin återgivning från konfigurationen. SwiftUI uppdaterar vyer som är beroende av ett tillstånd när det ändras. Exemplet behöver därför ingen timer, ingen callback för visning och ingen bakgrundsuppgift för att hålla det visuella tillståndet uppdaterat. State — Apple Developer Documentation
Animeringen tillhör visningsrektangeln. Den värdebaserade modifieraren tillämpar en animering när den övervakade andelen ändras. Varaktigheten på en kvarts sekund är ett illustrativt val för presentationen. Den beräknar inte uppgiftens förlopp. Ta bort modifieraren om omedelbara uppdateringar passar gränssnittet bättre. animation(_:value:) — Apple Developer Documentation
Det finns en dokumenterad begränsning för timerstyrt förlopp: Apple säger att datumrelativa förloppsvyer inte stöder anpassade stilar. Om du vill ha det här gradientutseendet använder du den numeriska initieraren för förlopp och låter aktivitetsägaren ange andelen som är klar. Om du behöver Apples automatiska datumrelativa uppdateringar använder du den initieraren med en vy som stöds. init(timerInterval:countsDown:label:currentValueLabel:) — Apple Developer Documentation
Kontrollera visningseffekten med en kort felsökningssekvens
Utför de här kontrollerna när du har byggt macOS-målet. De hjälper dig att skilja problem med färgmappning från problem med justering och storlek:
- Välj
Reset. Det grå spåret ska vara kvar och ingen gradient ska synas. - Välj
25%. Endast de tidiga gradientfärgerna ska synas. Om den orange ändpunkten syns tyder det på att själva gradienten görs kortare. - Välj
50%. Med de angivna färgstoppen ska visningsgränsen nå den lila mittpunkten. - Välj
Complete. Gradientens färger ska fylla spåret ända fram till orange. - Ändra fönstrets storlek vid 50 %. Den synliga delen ska fortfarande motsvara hälften av det nya spåret.
- Aktivera
Indeterminate. Den numeriska fyllningen ska ersättas av den inbyggda indikatorn, medan uppgifts- och aktuellvärdesetiketterna finns kvar.
Om visningen växer utåt från mitten kontrollerar du .mask(alignment: .leading). Om indikatorn tar upp för mycket vertikalt utrymme kontrollerar du ramen utanför GeometryReader. Om färgstoppens positioner ändras när förloppet ändras kontrollerar du att det bara är masken som använder multiplikatorn för hur mycket som är klart.
Den återanvändbara regeln är enkel: dimensionera gradienten för hela spåret och använd sedan en separat mask för att visa förloppet. Om ansvaret för dessa två delar hålls isär blir det enklare att resonera om partiellt förlopp, storleksändring och animering, samtidigt som förloppsvyns dokumenterade konfiguration behålls.
Källor
- 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