🛎️
SwiftUIとConcurrencyでいい感じのProgress Reporting UI
SwiftUIとSwift Concurrencyで簡単なProgress Reporting UIを作ります。
表示部分
まず簡単な表示部分の実装をします。進捗度を表す0から1の間の値を取るprogress
を用意します。
struct MyView: View {
@State private var progress: Float = 0 //0から1の間の値
var body: some View {
Button("Perform a Heavy Task") {
performTask()
}
ProgressView(value: progress)
}
func performTask() {
//下を参照
}
}
進捗更新部分
func performTask() {
let (stream, continuation) = AsyncStream<Float>.makeStream()
let progressReportingTask = Task.detached { //point 1
for await progress in stream {
Task { @MainActor in
withAnimation {
self.progress = progress
}
}
}
}
Task {
do {
defer { continuation.finish() } //point 2
let objects = ...
let totalObjectCount = objects.count
for index in 1...totalObjectCount {
let object = objects[index - 1]
try await object.someTask()
continuation.yield(Float(index) / Float(totalObjectCount)) //point 3
//await Task.yield() //point 4
}
} catch {
Task { @MainActor in
progressReportingTask.cancel() //point 5
withAnimation {
self.progress = 0
}
//エラーハンドリング(alertの表示など)
}
}
}
}
各ポイント
- 後でcancel処理の時に参照したいので、ここで
progressReportingTask
として保持しておきます。(ポイント5を参照) -
continuation.cancel()
を呼び忘れるとprogressReportingTask
が停止しないので、最後に呼び出すことをdefer
で明示しています。 - 今回は進捗度の計算を単純な割り算にしていますが、他の実装も考えられます。
continuation.yield(:)
に値を渡すと、progressReportingTask
の中のforループで処理されます。 - 長い時間にわたって
await
キーワードに遭遇しないままだとスレッド(正確にはスレッドではないらしい)を独占してしまって良くないので、適宜Task.yield()
を呼び出すように、とドキュメントにあります。try await object.someTask()
ではなくtry object.someTask()
であり、awaitが出現していないという場合にはこれが効果的です。必須ではありません。 - 大きく分けて
performTask
の中にはTask {}
が2つありますが、その間に親子関係がないので、上のdetached taskのキャンセルはプログラマ自ら行わなければなりません。
Discussion