コンテンツへスキップ

SwiftUIでAsyncButtonを作る

自動的なローディング状態、無効化、成功/失敗表示を備えた、非同期アクションを処理する再利用可能なボタンコンポーネント。

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の完全な実装では、設定可能な成功/失敗アニメーション、カスタマイズ可能なプログレスインジケーター、ボタンスタイルのサポートが追加されています。しかし、上記のコアパターンが最も一般的なケースをカバーしており、独自のプロジェクトに簡単に適用できます。

参考になりましたか?BlueskyMastodonでフォローして、Swiftのヒントやインディー開発の最新情報をチェックしてください。