Aby odsłaniać gradientowy pasek postępu SwiftUI bez kompresowania jego kolorów, zachowaj szerokość gradientu równą szerokości całego toru i animuj maskę wyrównaną do lewej krawędzi. Ten przykład dla macOS implementuje takie działanie w niestandardowym ProgressViewStyle.
Zadanie polega na zbudowaniu skalowalnego desktopowego wskaźnika postępu dla symulowanej aktywności. Przy ukończeniu w 25% powinien odsłonić pierwszą ćwiartkę stałej sekwencji kolorów. Kolejne kolory powinny pojawiać się wraz z postępem, a już widoczne kolory powinny pozostawać na swoich miejscach.
Dlaczego kolory się kompresują, gdy pasek postępu staje się krótszy?
Apple dokumentuje, że LinearGradient odwzorowuje punkt początkowy i końcowy na prostokąt ograniczający kształtu, który wypełnia. W rezultacie nadanie gradientowi szerokości proporcjonalnej do postępu zmienia również obszar, na którym rozkładają się jego kolory. Gradient o szerokości jednej czwartej nadal zawiera całą sekwencję kolorów. LinearGradient — dokumentacja Apple dla deweloperów
Aby uzyskać efekt odsłaniania, rozdziel dwa wymiary: szerokość gradientu odpowiada szerokości całego toru, a widoczna szerokość to szerokość toru pomnożona przez ułamek ukończenia. To rozwiązanie wynika z udokumentowanego działania gradientu, a nie z osobnego API paska postępu.
Rozważmy przykładowy tor o szerokości 320 punktów z niebieskim, fioletowym i pomarańczowym znacznikiem gradientu w znormalizowanych pozycjach 0, 0,5 i 1. Fioletowy punkt środkowy przez cały czas odsłaniania pozostaje w odległości 160 punktów:
| Postęp | Pełna szerokość gradientu | Szerokość maski | Widoczny efekt |
|---|---|---|---|
| 0% | 320 punktów | 0 punktów | Tylko tor |
| 25% | 320 punktów | 80 punktów | Pierwsza ćwiartka gradientu |
| 50% | 320 punktów | 160 punktów | Gradient aż do fioletowego punktu środkowego |
| 100% | 320 punktów | 320 punktów | Cała sekwencja kolorów |
Dzięki temu masz prosty test diagnostyczny: przy 25% końcowy pomarańczowy znacznik powinien być nadal ukryty. Jeśli pojawia się już przy przesuwającej się krawędzi, sprawdź, czy do gradientu nie jest stosowana szerokość zależna od postępu.
Zbuduj kompletny przykład gradientowego paska postępu dla macOS
Użyj celu aplikacji SwiftUI dla macOS z minimalną wersją systemu macOS 12 lub nowszą. Użyte poniżej przeciążenie mask(alignment:_:) uwzględniające wyrównanie jest dostępne od macOS 12. Apple opisuje je jako nakładanie kanału alfa innego widoku na maskowany widok. mask(alignment:_:) — dokumentacja Apple dla deweloperów
Zastąp wygenerowany plik punktu wejścia aplikacji tym kompletnym programem. Wygenerowany plik widoku zawartości może pozostać nieużywany. W celu aplikacji zachowaj dokładnie jedną deklarację @main, zgodnie z kontraktem punktu wejścia aplikacji Apple. App — dokumentacja Apple dla deweloperów
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
}
}
}
Niestandardowy styl implementuje makeBody(configuration:), udokumentowany punkt rozszerzeń określający wygląd widoku postępu. Renderuje oba przekazane etykiety i odczytuje ułamek ukończenia z konfiguracji. Umieszczenie stylizacji w tym protokole pozwala zachować interfejs ProgressView używany przez wywołujący kod. ProgressViewStyle — dokumentacja Apple dla deweloperów
Przykład rozpoczyna się od 25%. Użyj przycisków, aby przechodzić między powtarzalnymi punktami kontrolnymi, przeciągnij suwak, aby sprawdzić wartości pośrednie, i zmień rozmiar okna, aby sprawdzić układ. Te elementy sterujące zmieniają symulowany stan lokalny; nie reprezentują mierzonej pracy. Kod źródłowy został przejrzany, a obliczenia szerokości sprawdzone w Pythonie, ale programu SwiftUI nie skompilowano ani nie uruchomiono w środowisku badawczym.
Jak układ utrzymuje stabilność gradientu
GeometryReader mierzy przestrzeń przydzieloną wskaźnikowi. Apple opisuje go jako kontener, którego zawartość zależy od jego własnego rozmiaru i przestrzeni współrzędnych oraz który ma elastyczny preferowany rozmiar. Zastosowanie .frame(height: 14) poza nim nadaje temu elastycznemu kontenerowi jawną wysokość w stosie pionowym. GeometryReader — dokumentacja Apple dla deweloperów
Wewnątrz tego kontenera gradient otrzymuje pełną zmierzoną szerokość. Tylko prostokąt wewnątrz maski otrzymuje szerokość pomnożoną przez ułamek postępu. Gdy postęp się zmienia, ramka gradientu pozostaje stała dla danego rozmiaru okna.
Wyrównanie maski do .leading jest kluczowe: ustawia mniejszą maskę przy początkowej krawędzi gradientu. Parametr wyrównania ramki ustawia zawartość wewnątrz tej ramki; nie zastępuje wyrównania maski względem maskowanego widoku. Apple opisuje te kwestie jako odrębne zadania związane z wyrównaniem. mask(alignment:_:) — dokumentacja Apple dla deweloperów, frame(width:height:alignment:) — dokumentacja Apple dla deweloperów
Nieprzezroczysty prostokąt odsłania gradient w miejscach, w których się znajduje. Jego barwa nie decyduje o tym, które kolory gradientu zostaną odsłonięte, ponieważ maskowanie wykorzystuje alfę. Zastąpienie go przezroczystym prostokątem ukryłoby gradient. Umieść szary tor poza maską, aby pozostał widoczny przy zerowym postępie.
Końcowa kapsuła przycina tor i odsłonięty gradient do tego samego zewnętrznego kształtu. Apple dokumentuje przycinanie jako zachowanie fragmentów pokrytych kształtem przycinającym. W tym przykładzie przesuwająca się krawędź pozostaje prosta, aż dotrze do zaokrąglonego końca kapsuły; maska odsłaniania w kształcie kapsuły utworzyłaby zaokrągloną przesuwającą się krawędź. clipShape(_:style:) — dokumentacja Apple dla deweloperów
Zmiana rozmiaru okna zmienia pełną szerokość toru, więc gradient jest obliczany na nowo dla tej szerokości. „Stabilne kolory” oznaczają tutaj stabilne położenie względem toru podczas aktualizacji postępu, a nie stałe współrzędne ekranu przy zmianie rozmiaru okna.
Zachowaj wartości postępu, typy i działanie cyklu życia
Styl otrzymuje fractionCompleted jako opcjonalny Double. Apple określa jego zakres liczbowy jako od 0 do 1 i używa nil dla postępu nieokreślonego lub postępu względem przedziału czasu. Konwersja tego ułamka na CGFloat w obliczeniu szerokości sprawia, że granica typów liczbowych jest jawna. fractionCompleted — dokumentacja Apple dla deweloperów
Przykład defensywnie ogranicza skończone ułamki i kieruje brakujący lub nieskończony ułamek do standardowego wskaźnika postępu nieokreślonego. Obsługa wartości nieskończonych jest jawną decyzją przyjętą w tym przykładzie. Dzięki niej brak dostępnego ułamka nie jest interpretowany jako zerowy postęp. Wewnętrzny wskaźnik jawnie wybiera .circular, aby użyć wbudowanego stylu.
W miejscu wywołania podaj skończoną wartość ukończenia oraz skończoną, dodatnią wartość całkowitą. Inicjalizator oparty na wartości przyjmuje nil dla postępu nieokreślonego i dokumentuje ograniczanie wartości spoza zakresu do jego granic. W przykładzie użyto wartości całkowitej równej 1, a zakres suwaka ograniczono do tego samego przedziału. init(value:total:label:currentValueLabel:) — dokumentacja Apple dla deweloperów
Widok nadrzędny przechowuje stan postępu, a styl wyprowadza sposób renderowania z konfiguracji. SwiftUI aktualizuje zależne widoki po zmianie stanu. Dlatego ten przykład nie potrzebuje timera, wywołania zwrotnego pojawienia się widoku ani zadania w tle, aby utrzymywać stan wizualny. State — dokumentacja Apple dla deweloperów
Animacja dotyczy prostokąta odsłaniającego. Modyfikator ograniczony do wartości uruchamia animację po zmianie monitorowanego ułamka; wybrany czas trwania wynoszący ćwierć sekundy jest przykładową decyzją dotyczącą prezentacji. Nie oblicza postępu zadania. Usuń ten modyfikator, jeśli natychmiastowe aktualizacje lepiej pasują do interfejsu. animation(_:value:) — dokumentacja Apple dla deweloperów
Istnieje udokumentowane ograniczenie dotyczące postępu sterowanego timerem: Apple informuje, że widoki postępu względem dat nie obsługują niestandardowych stylów. Aby uzyskać ten gradientowy wygląd, użyj liczbowego inicjalizatora postępu i przekaż ułamek ukończenia z obiektu zarządzającego aktywnością. Jeśli potrzebujesz automatycznych aktualizacji względem daty oferowanych przez Apple, użyj tego inicjalizatora z obsługiwaną prezentacją. init(timerInterval:countsDown:label:currentValueLabel:) — dokumentacja Apple dla deweloperów
Sprawdź odsłanianie za pomocą krótkiej sekwencji diagnostycznej
Po zbudowaniu celu dla macOS wykonaj te czynności. Pomogą odróżnić problemy z mapowaniem kolorów od problemów z wyrównaniem i rozmiarem:
- Wybierz
Reset. Szary tor powinien pozostać widoczny, bez gradientu. - Wybierz
25%. Powinny pojawić się tylko początkowe kolory gradientu. Widoczny pomarańczowy koniec sugeruje, że skracany jest sam gradient. - Wybierz
50%. Przy podanych znacznikach granica odsłaniania powinna dotrzeć do fioletowego punktu środkowego. - Wybierz
Complete. Gradient powinien wypełnić tor aż do pomarańczowego końca. - Zmień rozmiar okna przy wartości 50%. Widoczna część powinna nadal stanowić połowę nowego rozmiaru toru.
- Włącz
Indeterminate. Wypełnienie liczbowe powinno ustąpić miejsca wbudowanemu wskaźnikowi, a etykiety zadania i bieżącej wartości powinny pozostać widoczne.
Jeśli odsłanianie zaczyna się od środka i rozszerza na zewnątrz, sprawdź .mask(alignment: .leading). Jeśli wskaźnik zajmuje nadmiernie dużo miejsca w pionie, sprawdź ramkę poza GeometryReader. Jeśli zmiana postępu przesuwa wszystkie znaczniki kolorów, sprawdź, czy mnożnik ukończenia jest używany tylko przez maskę.
Zasada, którą można ponownie wykorzystać, jest prosta: ustaw szerokość gradientu tak, aby odpowiadała całemu torowi, a następnie użyj osobno wymiarowanej maski, aby odsłaniać postęp. Rozdzielenie tych zadań ułatwia analizowanie częściowego ukończenia, zmiany rozmiaru i animacji, a zarazem zachowuje udokumentowaną konfigurację widoku postępu.
Źródła
- 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