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.
