Error alert presenting in SwiftUI simplified

Presenting error alerts in apps is essential to communicate failures to your end-users. The happy flow of apps is often implemented well, but the unhappy flow is equally important. Managing all kinds of error alerts can be frustrating if you have to present them all individually.

While building my apps, I’m constantly seeking generic solutions that I can continue using for future projects. Bundling all those extensions makes it easier for my future self to build apps and become more productive. One of the solutions I’ve written makes it easy to present an alert whenever you catch a localized error.

What is a localized error?

Before diving into presenting alerts, it’s good to know how a localized error differs from a standard error. A localized error provides localized messages describing the error and why it occurred. We can use this extra context to present an alert with an understandable explanation for our users.

If you want to learn more about error handling in Swift, I recommend reading Try Catch Throw: Error Handling in Swift with Code Examples.

Creating a generic error alert method

The ideal solution to me makes it effortless to present any alert based on a thrown error. The method determines whether we can show the error in an alert, so I only have to care about setting up a binding between my view and the potential errors.

In this example, I’m using an ArticleView that allows us to publish an article. Publishing can result in an error, which we want to present in an alert accordingly.

struct ArticleView: View {

    @StateObject var viewModel = ArticleViewModel()

    var body: some View {
        VStack {
            TextField(text: $viewModel.title, prompt: Text("Article title")) {
                Text("Title")
            }
            Button {
                viewModel.publish()
            } label: {
                Text("Publish")
            }
        }.errorAlert(error: $viewModel.error)
    }
}

A new errorAlert(error: Error?) view modifier is introduced, taking an optional error binding argument. Our view model defines the error binding that looks as follows:

final class ArticleViewModel: ObservableObject {
    enum Error: LocalizedError {
        case titleEmpty

        var errorDescription: String? {
            switch self {
            case .titleEmpty:
                return "Missing title"
            }
        }

        var recoverySuggestion: String? {
            switch self {
            case .titleEmpty:
                return "Article publishing failed due to missing title"
            }
        }
    }

    @Published var title: String = ""
    @Published var error: Swift.Error?

    func publish() {
        error = Error.titleEmpty
    }
}

For this example, I’m always throwing an error upon publishing. In this case, we update the error binding with an Error.titleEmpty. The error conforms to the localized error protocol and provides a description and a recovery suggestion. We can now use this metadata to create a generic view extension for presenting alerts.

extension View {
    func errorAlert(error: Binding<Error?>, buttonTitle: String = "OK") -> some View {
        let localizedAlertError = LocalizedAlertError(error: error.wrappedValue)
        return alert(isPresented: .constant(localizedAlertError != nil), error: localizedAlertError) { _ in
            Button(buttonTitle) {
                error.wrappedValue = nil
            }
        } message: { error in
            Text(error.recoverySuggestion ?? "")
        }
    }
}

The extension method takes a binding to an optional error as an argument. We can use this binding to read out the error, as well as to set the error back to nil so our alert will be dismissed.

We’re using the default alert modifier that requires us to pass a generic LocalizedError. We have to pass our error argument into a custom LocalizedAlertError struct to prevent the following error:

Protocol ‘LocalizedError’ as a type cannot conform to the protocol itself

This structure works as a facade and looks as follows:

struct LocalizedAlertError: LocalizedError {
    let underlyingError: LocalizedError
    var errorDescription: String? {
        underlyingError.errorDescription
    }
    var recoverySuggestion: String? {
        underlyingError.recoverySuggestion
    }

    init?(error: Error?) {
        guard let localizedError = error as? LocalizedError else { return nil }
        underlyingError = localizedError
    }
}

We can return nil by using an optional initializer, allowing us not to present an alert if the thrown error isn’t localized. The binding constant takes the result as input to determine whether the alert should be presented or not:

.constant(localizedAlertError != nil)

Altogether, this implementation allows us to show an error-based alert as follows:

struct ArticleView: View {

    @StateObject var viewModel = ArticleViewModel()

    var body: some View {
        VStack {
            TextField(text: $viewModel.title, prompt: Text("Article title")) {
                Text("Title")
            }
            Button {
                viewModel.publish()
            } label: {
                Text("Publish")
            }
        }.errorAlert(error: $viewModel.error)
    }
}

Resulting in an alert representing any localized errors:

An alert represents a localized error.
An alert represents a localized error.

Conclusion

Presenting errors to our end-users is essential to provide feedback in unhappy flows. By creating a generic solution, we allow ourselves to simplify this kind of implementation. The LocalizedError protocol allows presenting an alert for errors that come out of 3rd party frameworks that aren’t in our control.

If you like to improve your SwiftUI knowledge even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!

 

Featured SwiftLee Jobs

Loading RSS Feed

Browse more Swift related Jobs, or add your own on SwiftLee Jobs