Skip to content

Commit d4c6d85

Browse files
authored
Merge pull request #35 from wmorgue/translation_progressview
Translation ProgressView article.
2 parents b242052 + 1b3568c commit d4c6d85

File tree

4 files changed

+194
-1
lines changed

4 files changed

+194
-1
lines changed

.yaspellerrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"unownedExecutor",
5757
"UnownedSerialExecutor",
5858
"inBillingRetryPeriod",
59+
"RoundedProgressViewStyle",
5960
"inGracePeriod",
6061
"AppStore",
6162
"willAutoRenew",

en/meta/articles.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,17 @@
7777
],
7878
"updated_date" : "10.02.2022",
7979
"added_date" : "10.02.2022"
80+
},
81+
"mastering-progressview-swiftui" : {
82+
"title" : "ProgressView in SwiftUI",
83+
"description" : "How ProgressView works. How to customize the appearance: spinner and progress bar.",
84+
"category" : "swiftui",
85+
"author" : "wmorgue",
86+
"editors" : ["ivanvorobei"],
87+
"keywords" : [
88+
"ProgressView"
89+
],
90+
"updated_date": "11.02.2022",
91+
"added_date": "11.02.2022"
8092
}
8193
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
To indicate the background work in the application use `ProgressView`.
2+
3+
## Indeterminate progress
4+
5+
Let's add a `ProgressView()`:
6+
7+
```swift
8+
struct ContentView: View {
9+
10+
var body: some View {
11+
VStack(spacing: 40) {
12+
ProgressView()
13+
Divider()
14+
ProgressView("Loading")
15+
.tint(.pink)
16+
}
17+
}
18+
}
19+
```
20+
21+
[Indeterminate Activity Indicator](https://cdn.ivanvorobei.by/websites/sparrowcode.io/mastering-progressview-swiftui/indeterminate_activity_indicator.mov)
22+
23+
By default `SwiftUI` defines a rotating loading bar (spinner). The modifier `.tint()` changes the color of the bar.
24+
25+
## Determinate progress
26+
27+
Initialize the view with another indicator:
28+
29+
```swift
30+
struct ContentView: View {
31+
32+
let totalProgress: Double = 100
33+
@State private var progress = 0.0
34+
35+
var body: some View {
36+
VStack(spacing: 40) {
37+
currentTextProgress
38+
39+
ProgressView(value: progress, total: totalProgress)
40+
.padding(.horizontal, 40)
41+
42+
loadResetButtons
43+
}
44+
}
45+
}
46+
47+
extension ContentView {
48+
49+
private var currentTextProgress: Text {
50+
switch progress {
51+
case 5..<totalProgress: return Text("Current progress: \(Int(progress))%")
52+
case totalProgress...: return Text("Loading complete")
53+
default: return Text("Start loading")
54+
}
55+
}
56+
57+
private var loadResetButtons: some View {
58+
HStack(spacing: 20) {
59+
Button("Load more") {
60+
withAnimation { progress += Double.random(in: 5...20) }
61+
}
62+
.disabled(!progress.isLessThanOrEqualTo(totalProgress))
63+
64+
Button(role: .destructive) {
65+
progress = 0
66+
} label: {
67+
Text("Reset")
68+
}
69+
.tint(.red)
70+
.disabled(progress.isZero)
71+
}
72+
.buttonStyle(.bordered)
73+
}
74+
}
75+
```
76+
77+
[Determinate Activity Indicator](https://cdn.ivanvorobei.by/websites/sparrowcode.io/mastering-progressview-swiftui/determinate_activity_indicator.mov)
78+
79+
Pressing the `Load more` button starts the download. The text shows the current progress and the `Reset` button will become available to tap and reset. When the download is finished, the text on the screen will let you know. The `Load more` button will become inactive.
80+
81+
Let's make a progress simulation with a timer:
82+
83+
```swift
84+
// filename: TimerProgressView.swift
85+
86+
struct TimerProgressView: View {
87+
88+
let timer = Timer
89+
.publish(every: 0.05, on: .main, in: .common)
90+
.autoconnect()
91+
92+
let downloadTotal: Double = 100
93+
@State private var progress: Double = 0
94+
95+
var body: some View {
96+
VStack(spacing: 40) {
97+
Text("Downloading: \(Int(progress))%")
98+
99+
ProgressView(value: progress, total: downloadTotal)
100+
.tint(progress < 50 ? .pink : .green)
101+
.padding(.horizontal)
102+
.onReceive(timer) { _ in
103+
if progress < downloadTotal { progress += 1 }
104+
}
105+
}
106+
}
107+
}
108+
```
109+
110+
[Timer Progress](https://cdn.ivanvorobei.by/websites/sparrowcode.io/mastering-progressview-swiftui/timer_progress.mov)
111+
112+
The event is called several times by a timer. Timer source code:
113+
114+
```swift
115+
let timer = Timer.publish(every: 0.05, on: .main, in: .common).autoconnect()
116+
```
117+
118+
The timer is triggered every 0.05 seconds (50 milliseconds). The timer must run in the main thread and the common run loop. The run loop allows code to be processed when the user does something (presses a button). The timer starts counting down instantly.
119+
120+
When `progress` reaches the `downloadTotal` value, the timer stops.
121+
When it reaches 50% of the download, the indicator changes color into green.
122+
123+
The `ProgressView` looks like a loading bar that fills from left to right.
124+
This is how we show the user that the loading progress depends on the size of the file.
125+
126+
A description of the `publish` method is available in [Apple documentation](https://developer.apple.com/documentation/foundation/timer/3329589-publish). More initializers can be found in the Xcode documentation or on the [website](https://developer.apple.com/documentation/swiftui/progressview).
127+
128+
![Documentation SwiftUI ProgressView](https://cdn.ivanvorobei.by/websites/sparrowcode.io/mastering-progressview-swiftui/progressview_init.png)
129+
130+
## Styling Progress Views
131+
132+
A custom design for `ProgressView` is created using the protocol `ProgressViewStyle`, which we need to inherit from it. Let's declare a structure `RoundedProgressViewStyle` which contains method `makeBody()` and takes configuration parameter for the style:
133+
134+
```swift
135+
struct RoundedProgressViewStyle: ProgressViewStyle {
136+
137+
let color: Color
138+
139+
func makeBody(configuration: Configuration) -> some View {
140+
let fractionCompleted = configuration.fractionCompleted ?? 0
141+
142+
RoundedRectangle(cornerRadius: 18)
143+
.frame(width: CGFloat(fractionCompleted) * 200, height: 22)
144+
.foregroundColor(color)
145+
.padding(.horizontal)
146+
}
147+
}
148+
```
149+
150+
Let's go back to `TimerProgressView.swift` and pass `RoundedProgressViewStyle(color: .cyan)` to the `.progressViewStyle()` modifier. Now the code looks like this:
151+
152+
```swift
153+
struct TimerProgressView: View {
154+
155+
let timer = Timer
156+
.publish(every: 0.05, on: .main, in: .common)
157+
.autoconnect()
158+
159+
let downloadTotal: Double = 100
160+
@State private var progress: Double = 0
161+
162+
var body: some View {
163+
VStack(spacing: 40) {
164+
Text("Downloading: \(Int(progress))%")
165+
166+
ProgressView(value: progress, total: downloadTotal)
167+
.onReceive(timer) { _ in
168+
if progress < downloadTotal { progress += 1 }
169+
}
170+
.progressViewStyle(
171+
RoundedProgressViewStyle(color: .cyan)
172+
)
173+
}
174+
}
175+
}
176+
```
177+
178+
Progress begins not from left to right, but from the middle in opposite directions.
179+
180+
[RoundedProgressViewStyle](https://cdn.ivanvorobei.by/websites/sparrowcode.io/mastering-progressview-swiftui/rounded_progress_view.mov)

ru/meta/authors.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
},
3232
"wmorgue": {
3333
"name": "Никита Россик",
34-
"description": "Увлекаюсь обратной разработкой под iOS.",
34+
"description": "Увлекаюсь разработкой под .",
3535
"avatar": "https://cdn.ivanvorobei.by/websites/sparrowcode.io/authors/wmorgue.jpg",
3636
"buttons": [
3737
{

0 commit comments

Comments
 (0)