Wesley de Groot's Blog
Building an Asynchronous Button in SwiftUI

Back

Sometimes, we need to perform asynchronous tasks when a button is tapped in a SwiftUI application. For example, fetching data from a network request, saving data to a database, or performing any long-running operation. In this tutorial, we'll explore how to create an asynchronous button in SwiftUI that performs an asynchronous task when tapped.

Why Use Asynchronous Buttons?

Asynchronous tasks are essential for operations that take time to complete, such as network requests, file I/O, or any long-running computations. By using asynchronous buttons, we can keep the UI responsive and provide feedback to users while the task is being performed.

Creating an Async Button

In this code snippet, we'll create an AsyncButton view that performs an asynchronous task when tapped. The button will display a loading indicator while the task is in progress and disable user interaction to prevent multiple taps.
This is a simple example of how to create an asynchronous button in SwiftUI:

import SwiftUI

/// A button that performs an asynchronous task when tapped.
struct AsyncButton<Label: View>: View {
    /// The asynchronous action to perform when the button is tapped.
    var action: () async -> Void

    /// The label of the button.
    @ViewBuilder
    var label: () -> Label

    /// Whether the task is currently being performed.
    @State
    private var isPerformingTask = false

    var body: some View {
        Button(action: {
            // When the button is tapped, we are performing a task
            isPerformingTask = true

            // Perform the task asynchronously
            Task {
                // Perform the asynchronous task
                await action()

                // After the task is completed, we are no longer performing a task
                isPerformingTask = false
            }
        }) {
            HStack {
                // Show a loading indicator while the task is in progress
                if isPerformingTask {
                    ProgressView()
                        .controlSize(.mini)
                }

                // Show the label of the button
                label()
            }
        }
        // Disable the button while the task is in progress
        .disabled(isPerformingTask)
    }
}

/// Example usage:
struct ContentView: View {
    var body: some View {
        AsyncButton(action: fetchData) {
            Text("Fetch Data")
        }
    }

    func fetchData() async { // <2>
        // Simulate a network request
        try? await Task.sleep(nanoseconds: 2 * 1_000_000_000)
        print("Data fetched")
    }
}

Improving the Async Button

The AsyncButton view can be further customized to handle errors and provide feedback to the user. For example, we can display an error message if the task fails or show a success message when the task completes successfully. We can also add animations to enhance the user experience.

Here's an improved version of the AsyncButton view that handles errors and provides feedback to the user:

import SwiftUI

/// A button that performs an asynchronous task when tapped.
struct AsyncButton<Label: View>: View {
    /// The asynchronous action to perform when the button is tapped.
    var action: () async throws -> Void

    /// The label of the button.
    @ViewBuilder
    var label: () -> Label

    /// Whether the task is currently being performed.
    @State
    private var isPerformingTask = false

    /// The error message to display if the task fails.
    @State
    private var errorMessage: String?

    /// Whether to show the alert.
    @State
    private var showAlert = false

    var body: some View {
        Button(action: {
            // When the button is tapped, we are performing a task
            isPerformingTask = true

            // Perform the task asynchronously
            Task {
                do {
                    // Perform the asynchronous task
                    try await action()

                    // If the task completes successfully, clear the error message (if any)
                    errorMessage = nil
                } catch {
                    // If the task fails, display the error message
                    errorMessage = error.localizedDescription
                }

                // After the task is completed, we are no longer performing a task
                isPerformingTask = false

                // Show the alert if there is an error
                showAlert = errorMessage != nil
            }
        }) {
            HStack {
                // Show a loading indicator while the task is in progress
                if isPerformingTask {
                    // Use a mini progress view
                    ProgressView()
                        .controlSize(.mini)
                }

                // Show the button label
                label()
            }
        }
        // Disable the button while the task is in progress
        .disabled(isPerformingTask)
        // Show an alert with the error message if the task fails
        .alert(isPresented: $showAlert) {
            // Display an alert with the error message
            Alert(
                title: Text("Error"), 
                message: Text(errorMessage ?? ""), 
                dismissButton: .default(Text("OK"))
            )
        }
    }
}

Conclusion

Creating an asynchronous button in SwiftUI is straightforward and enhances the user experience by keeping the UI responsive. By leveraging Swift's concurrency model, we can perform tasks asynchronously and handle errors gracefully. This approach ensures that our applications remain smooth and user-friendly, even when performing complex operations.

Resources

Read more

Share


Share Mastodon Twitter LinkedIn Facebook
x-twitter mastodon github linkedin discord threads instagram whatsapp bluesky square-rss sitemap