AsyncButtonの必要性
標準のSwiftUI Buttonアクションは同期的です。ネットワークリクエスト、データベースへの書き込み、StoreKitでの購入など、非同期操作を実行する必要がある場合、手動でTaskを管理し、ローディング状態を追跡し、ボタンを無効化し、エラーを処理する必要があります。このボイラープレートはアプリ内のすべての非同期ボタンで繰り返されます。
これらすべてを単一の再利用可能なビューにまとめたAsyncButtonコンポーネントを構築しました。
シンプルな実装例
基本的な考え方は以下の通りです:
struct AsyncButton<Label: View>: View {
let action: () async throws -> Void
let label: () -> Label
@State private var isRunning = false
@State private var result: Result<Void, Error>?
var body: some View {
Button {
isRunning = true
Task {
do {
try await action()
result = .success(())
} catch {
result = .failure(error)
}
isRunning = false
}
} label: {
HStack(spacing: 8) {
label()
if isRunning {
ProgressView()
}
}
}
.disabled(isRunning)
}
}ボタンは内部でTaskを作成するため、呼び出し側はアクションクロージャ内で直接awaitを使用できます。タスクの実行中は、ラベルの横にProgressViewが表示され、重複送信を防ぐためにボタンが無効化されます。result状態は、チェックマーク、色のフラッシュ、シェイクアニメーションなど、成功や失敗のインジケーターを駆動できます。
使い方
使い方は自然です:
AsyncButton {
try await viewModel.submitOrder()
} label: {
Text("Place Order")
}手動の状態管理も、呼び出し箇所でのTask作成も不要です。HandySwiftUIの完全な実装では、設定可能な成功/失敗アニメーション、カスタマイズ可能なプログレスインジケーター、ボタンスタイルのサポートが追加されています。しかし、上記のコアパターンが最も一般的なケースをカバーしており、独自のプロジェクトに簡単に適用できます。
