|
| 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 | + |
| 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) |
0 commit comments