Skip to content

Building an AsyncButton in SwiftUI

A reusable button component that handles async actions with automatic loading state, disabling, and success/failure indication.

The Need for AsyncButton

Standard SwiftUI Button actions are synchronous. When you need to perform an async operation – a network request, a database write, a StoreKit purchase – you end up manually managing a Task, tracking loading state, disabling the button, and handling errors. This boilerplate repeats across every async button in your app.

I built an AsyncButton component that wraps all of this into a single reusable view.

A Simplified Implementation

Here is the core idea:

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)
   }
}

The button creates a Task internally, so callers can use await directly in the action closure. While the task runs, a ProgressView appears next to the label and the button is disabled to prevent duplicate submissions. The result state can drive success or failure indicators – a checkmark, a color flash, or a shake animation.

Usage

Using it feels natural:

AsyncButton {
   try await viewModel.submitOrder()
} label: {
   Text("Place Order")
}

No manual state management, no Task creation at the call site. The full implementation in HandySwiftUI adds configurable success/failure animations, customizable progress indicators, and support for button styles. But the core pattern above covers the most common case and is straightforward to adapt to your own projects.

Found this helpful? Follow me on Bluesky and Mastodon for more Swift tips and indie dev updates.