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.

Categorized in:

SwiftUI,