Swift Concurrency became a vital part of my development stack. I leverage the power of the new Swift Concurrency features like async/await and task groups almost everywhere. SwiftUI Button type doesn’t support Swift Concurrency out of the box, but it is flexible enough to allow us to build a button type supporting Swift Concurrency.
Basic Example:
To start, let’s look at a simple example where a button triggers an asynchronous task:
struct ExampleView: View {
@State private var counter = 0
var body: some View {
VStack {
Text("\(counter)")
Button {
Task {
await heavyUpdate()
}
} label: {
Text("Increment")
}
}
}
private func heavyUpdate() async {
do {
print("update started")
try await Task.sleep(nanoseconds: 3 * 1_000_000_000)
counter += 1
print("update finished")
} catch {
print("update cancelled")
}
}
}
In this example, the heavyUpdate
function simulates a long-running task by sleeping for a few seconds. The button triggers this task using the Task
initializer, ensuring the UI remains responsive.
Disabling the Button During Async Task
To prevent multiple task invocations, you can disable the button while the task is running:
struct ExampleView: View {
@State private var isRunning = false
@State private var counter = 0
var body: some View {
VStack {
Text("\(counter)")
Button {
isRunning = true
Task {
await heavyUpdate()
isRunning = false
}
} label: {
Text("Increment")
}
.disabled(isRunning)
}
}
private func heavyUpdate() async {
do {
print("update started")
try await Task.sleep(nanoseconds: 3 * 1_000_000_000)
counter += 1
print("update finished")
} catch {
print("update cancelled")
}
}
}
Here, the isRunning
state variable is used to track the task’s status, disabling the button when the task is active.
Creating a Custom AsyncButton
To make this logic reusable, you can encapsulate it in a custom AsyncButton
view:
struct AsyncButton<Label: View>: View {
let action: () async -> Void
let label: Label
@State private var isRunning = false
init(action: @escaping () async -> Void, @ViewBuilder label: () -> Label) {
self.action = action
self.label = label()
}
var body: some View {
Button {
isRunning = true
Task {
await action()
isRunning = false
}
} label: {
ZStack {
label.opacity(isRunning ? 0 : 1)
if isRunning {
ProgressView()
}
}
}
.disabled(isRunning)
}
}
This AsyncButton
component handles the asynchronous action and displays a ProgressView
while the task is running. The button is disabled during the task to prevent multiple invocations.
Usage Example
You can use the AsyncButton
in your views like this:
struct AsyncButtonExampleView: View {
@State private var counter = 0
var body: some View {
VStack {
Text("\(counter)")
AsyncButton {
do {
try await Task.sleep(nanoseconds: 3 * 1_000_000_000)
counter += 1
} catch {
print("Task cancelled")
}
} label: {
Text("Increment")
}
.controlSize(.large)
.buttonStyle(.borderedProminent)
}
}
}
In this example, the AsyncButton
increments the counter after a delay, demonstrating how to integrate asynchronous operations seamlessly within SwiftUI.
Conclusion
Creating an asynchronous button in SwiftUI involves using the async
and await
keywords to handle non-blocking tasks. By encapsulating this logic in a custom AsyncButton
component, you can create reusable and responsive UI elements that handle asynchronous actions efficiently. This approach not only improves code readability but also enhances the user experience by providing visual feedback during long-running operations.