🛎️

SwiftUIとConcurrencyでいい感じのProgress Reporting UI

2024/03/25に公開

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の表示など)
            }
        }
    }
}

各ポイント

  1. 後でcancel処理の時に参照したいので、ここでprogressReportingTaskとして保持しておきます。(ポイント5を参照)
  2. continuation.cancel()を呼び忘れるとprogressReportingTaskが停止しないので、最後に呼び出すことをdeferで明示しています。
  3. 今回は進捗度の計算を単純な割り算にしていますが、他の実装も考えられます。continuation.yield(:)に値を渡すと、progressReportingTaskの中のforループで処理されます。
  4. 長い時間にわたってawaitキーワードに遭遇しないままだとスレッド(正確にはスレッドではないらしい)を独占してしまって良くないので、適宜Task.yield()を呼び出すように、とドキュメントにあります。try await object.someTask()ではなくtry object.someTask()であり、awaitが出現していないという場合にはこれが効果的です。必須ではありません。
  5. 大きく分けてperformTaskの中にはTask {}が2つありますが、その間に親子関係がないので、上のdetached taskのキャンセルはプログラマ自ら行わなければなりません。

Discussion