Alert and LocalizedError in SwiftUI

Everything can’t go exactly as planned and therefore, at some point, there is a need for presenting localized error messages to the user. Let’s take a look at how to add custom error type what provides error description, failure reason and recovery suggestion and presenting it in SwiftUI view.

Adding custom error type

Custom error type is needed when we want to propagate errors using Swift’s error handling mechanism. Custom error types need to, at minimum, conform to Error protocol which defines localizedDescription property. If we would like to provide more information to users, including recovery suggestions, then we need to use LocalizedError instead. LocalizedError inherits from Error and defines additional properties which are intended for describing the error further. Note that LocalizedError is very similar to NSError: errorDescription, failureReason, recoverySuggestion, helpAnchor are all represented by NSError.UserInfoKey.

In the example app we’ll use LoginError currently definiing only one error: incorrectPassword.

enum LoginError: LocalizedError {
    case incorrectPassword // invalidUserName etc
    
    var errorDescription: String? {
        switch self {
        case .incorrectPassword:
            return "Failed logging in account"
        }
    }
    
    var failureReason: String? {
        switch self {
        case .incorrectPassword:
            return "Entered password was incorrect"
        }
    }
    
    var recoverySuggestion: String? {
        switch self {
        case .incorrectPassword:
            return "Please try again with different password"
        }
    }
}

Presenting error in SwiftUI

Custom error defined, the next step is to present the error using SwiftUI’s alert view modifier: alert(isPresented:content:). Alert view modifier requires boolean binding and Alert container defining title, optional message, and buttons. In the example below, error is handled by the view model and Alert itself is created using convenience initializer which we’ll look at a bit later. Convenience initializer makes the view implementation more readable and reduces code duplication.

struct ContentView: View {
    @ObservedObject var viewModel: ContentViewModel
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Alert Views")
            Button(action: viewModel.showAlertView) {
                Text("Show Alert View")
            }
        }.alert(isPresented: viewModel.isPresentingAlert, content: {
            Alert(localizedError: viewModel.activeError!)
        })
    }
}

Alert view modifier requires a boolean binding controlling if the alert is visible or not. When alert is dismissed, SwiftUI automatically calls the binding with false, indicating that the alert should not be visible anymore. Note that force unwrap is safe here because view model makes sure isPresentingAlert never returns true when underlying error is nil.

final class ContentViewModel: ObservableObject {
    @Published private(set) var activeError: LocalizedError?

    var isPresentingAlert: Binding<Bool> {
        return Binding<Bool>(get: {
            return self.activeError != nil
        }, set: { newValue in
            guard !newValue else { return }
            self.activeError = nil
        })
    }
        
    func showAlertView() {
        activeError = LoginError.incorrectPassword
    }
}

Boolean binding is created manually and implemented in such way that when activeError is set, isPresentingAlert returns true. When alert is dismissed, set will clear the current active error. This approach makes it simple to handle any errors conforming to LocalizedError in the view model. Like mentioned before, LocalizedError enables us to add detailed information about the alert and we can use that when creating the Alert. Let’s take a look on it next.

extension Alert {
    init(localizedError: LocalizedError) {
        self = Alert(nsError: localizedError as NSError)
    }
    
    init(nsError: NSError) {
        let message: Text? = {
            let message = [nsError.localizedFailureReason, nsError.localizedRecoverySuggestion].compactMap({ $0 }).joined(separator: "\n\n")
            return message.isEmpty ? nil : Text(message)
        }()
        self = Alert(title: Text(nsError.localizedDescription),
                     message: message,
                     dismissButton: .default(Text("OK")))
    }
}

Alert’s extension has initializers both for LocalizedError and NSError. NSError is used a lot in Objective-C frameworks so there is high probability that we need to present NSError in the future as well. Here, we can use Swift language’s built-in support of converting Swift error type to NSError and therefore we can implement convenience method only once for NSError. LocalizedError can be bridged to NSError and Swift compiler takes care of keeping the information about the error. In this implementation, I decided to include both the failureReason and recoverySuggestion when creating the message for Alert. This enables custom error types to choose how much information they provide (choosing which properties return text). Moreover, it is better to show as much information about the error as possible.

LoginError.incorrectPassword presented by Alert in SwiftUI

Summary

We created a custom error type and used LocalizedError instead of Error for making it suitable for displaying as an alert. We looked into how to use alert view modifier and MVVM together and introduced design pattern for easy alert presentation. If you need action sheet, then follow similar steps but use actionSheet view modifier with ActionSheet container.

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

SwiftUIAlertView (Xcode 11.4 beta 2)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s