Installation • Usage • Issues • Contributing • License
Error Handler based on localized & healable (recoverable) errors without the overhead of NSError (which you would have when using LocalizedError & RecoverableError instead).
When developing a new feature for an App developers often need to both have presentable results fast and at the same time provide good user feedback for edge cases like failed network requests or invalid user input.
While there are many ways to deal with such situations, MungoHealer provides a straightforward and Swift-powered approach that uses system alerts for user feedback by default, but can be easily customized to use custom UI when needed.
Here's a very simple example of basic error handling without MungoHealer:
func login(success: (String) -> Void) {
guard let username = usernameLabel.text, !username.isEmpty else {
let alertCtrl = UIAlertController(title: "Invalid User Input", message: "Please enter a username.", preferredStyle: .alert)
alertCtrl.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
viewController.present(alertCtrl, animated: true, completion: nil)
return
}
guard let password = passwordLabel.text, !password.isEmpty else {
let alertCtrl = UIAlertController(title: "Invalid User Input", message: "Please enter a password.", preferredStyle: .alert)
alertCtrl.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
viewController.present(alertCtrl, animated: true, completion: nil)
return
}
guard let apiToken = getApiToken(username, password) else {
let alertCtrl = UIAlertController(title: "Invalid User Input", message: "Username and password did not match.", preferredStyle: .alert)
alertCtrl.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
viewController.present(alertCtrl, animated: true, completion: nil)
return
}
success(apiToken)
)
Using MungoHealer the above code becomes this:
func login(success: (String) -> Void) {
mungo.do {
guard let username = usernameLabel.text, !username.isEmpty else {
throw MungoError(source: .invalidUserInput, message: "Please enter a username.")
}
guard let password = passwordLabel.text, !password.isEmpty else {
throw MungoError(source: .invalidUserInput, message: "Please enter a password.")
}
guard let apiToken = getApiToken(username, password) else {
throw MungoError(source: .invalidUserInput, message: "Username and password did not match.")
}
success(apiToken)
}
)
Installing via Carthage & CocoaPods are both supported.
Support for SPM is currently not possible as this framework uses UIKit.
Please also have a look at the MungoHealer iOS-Demo
project in the subfolder Demos
for a live usage example.
MungoHealer is based on Swifts built-in error handling mechanism. So before we can throw any useful error message when something goes wrong, we need to define our errors.
MungoHealer can deal with any errors thrown by system frameworks or third party libraries alike, but to make use of the user feedback automatics, you need to implement one of MungoHealers error type protocols:
A localized error type without the overhead of NSError – truly designed for Swift. Use this for any error you want to provide localized user feedback for.
Requirements
source: ErrorSource
A classification of the errors source. MungoHealer will automatically provide an alert title based on it. The available options are:
.invalidUserInput
.internalInconsistency
.externalSystemUnavailable
.externalSystemBehavedUnexpectedlyBased
The options are explained in detail here.
errorDescription: String
A localized message describing what error occurred. This will be presented to the user as the alerts message by default when the error occurs.
debugDescription: String?
An optional message describing the error in more technical detail for debugging purposes. This will not be presented to the user and so for only logged.
Example
struct PasswordValidationError: BaseError {
let errorDescription = "Your password confirmation didn't match your password. Please try again."
let source = ErrorSource.invalidUserInput
}
A non-healable (non-recoverable) & localized fatal error type without the overhead of NSError – truly designed for Swift. Use this as an alternative for fatalError
and tasks like force-unwrapping when you don't expect a nil
value and therefore don't plan to heal (recover from).
Note that throwing a FatalError
will crash your app, just like fatalError()
or force-unwrapping nil
would. The difference is, that here the user is first presented with an error message which is a better user experience. Additionally, before the app crashes you have the chance to do any cleanup or reporting tasks via a callback if you need to.
It is highly recommended to keep the suffix FatalError
in your custom errors class name to clearly communicate that throwing this will crash the app.
Requirements
FatalError
has the exact same requirements as BaseError
. In fact its type declaration is as simple as this:
public protocol FatalError: BaseError {}
The only difference is semantics – the data provided by a FatalError
will be used for alert title & message as well. But confirming the alert will crash the app.
Example
struct UnexpectedNilFatalError: FatalError {
let errorDescription = "An unexpected data inconsistency has occurred. App execution can not be continued."
let source = ErrorSource.internalInconsistency
}
A healable (recoverable) & localized error type without the overhead of NSError – truly designed for Swift. Use this for any edge-cases you can heal (recover from) like network timeouts (healing via Retry), network unauthorized responses (healing via Logout) etc.
Requirements
HealableError
extends BaseError
and therefore has the same requirements. In addition to that, you need to add:
healingOptions: [HealingOption]
Provides an array of possible healing options to present to the user. A healing option consists of the following:
style: Style
: The style of the healing option. One of:.normal
,.recommended
or.destructive
title: String
: The title of the healing option.handler: () -> Void
: The code to be executed when the user chooses the healing option.
Note that you must provide at least one healing option.
Example
struct NetworkUnavailableError: HealableError {
private let retryClosure: () -> Void
init(retryClosure: @escaping () -> Void) {
self.retryClosure = retryClosure
}
let errorDescription = "Could not connect to server. Please check your internet connection and try again."
let source = ErrorSource.externalSystemUnavailable
var healingOptions: [HealingOption] {
let retryOption = HealingOption(style: .recommended, title: "Try Again", handler: retryClosure)
let cancelOption = HealingOption(style: .normal, title: "Cancel", handler: {})
return [retryOption, cancelOption]
}
}
MungoHealer provides one basic implementation of each error protocol which you can use for convenience so you don't have to write a new error type for simple message errors. These are:
MungoError
- Implements
BaseError
init
takessource: ErrorSource
&message: String
Example Usage:
func fetchImage(urlPath: String) {
guard let url = URL(string: urlPath) else {
throw MungoError(source: .invalidUserInput, message: "Invalid Path")
}
// ...
}
MungoFatalError
- Implements
FatalError
init
takessource: ErrorSource
&message: String
Example Usage:
func fetchImage(urlPath: String) {
guard let url = URL(string: urlPath) else {
throw MungoFatalError(source: .invalidUserInput, message: "Invalid Path")
}
// ...
}
MungoHealableError
- Implements
HealableError
init
takessource: ErrorSource
&message: String
init
additionally takeshealOption: HealOption
Example Usage:
func fetchImage(urlPath: String) {
guard let url = URL(string: urlPath) else {
let healingOption = HealingOption(style: .recommended, title: "Retry") { [weak self] in self?.fetchImage(urlPath: urlPath) }
throw MungoHealableError(source: .invalidUserInput, message: "Invalid Path", healingOption: healingOption)
}
// ...
}
MungoHealer makes handling errors easier by providing the ErrorHandler
protocol and a default implementation of it based on alert views, namely AlertLogErrorHandler
.
The easiest way to get started with MungoHealer is to use a global variable and set it in your AppDelegate.swift like this:
import MungoHealer
import UIKit
var mungo: MungoHealer!
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
configureMungoHealer()
return true
}
private func configureMungoHealer() {
let errorHandler = AlertLogErrorHandler(window: window!, logError: { print("Error: \($0)") })
mungo = MungoHealer(errorHandler: errorHandler)
}
}
Note that the following steps were taken in the above code:
- Add
import MungoHealer
at the top - Add
var mungo: MungoHealer!
global variable - Add a private
configureMungoHealer()
method - Provide your preferred
logError
handler (e.g. SwiftyBeaver) - Call
configureMungoHealer()
on app launch
As you can see, the AlertLogErrorHandler
receives two parameters: The first is the window
so it can find the current view controller to present alerts within. The second is a error log handler – the AlertLogErrorHandler
not only presents alerts when there is a localized error, but it will also log all errors calling the logError
handler with the errors localized description.
While starting with the AlertLogErrorHandler
is recommended by default, you might of course want to handle errors differently then just with system alerts & logs. For these cases, you just need to implement your own error handler by conforming to ErrorHandler
which requires the following methods:
handle(error: Error)
: Called for "normal" error types.handle(baseError: BaseError)
: Called for base error types.handle(fatalError: FatalError)
: Called for fatal error types – App should crash at the end of this method.handle(healableError: HealableError)
: Calles for healable error types.
See the implementation of AlertLogErrorHandler
here for a working example.
Note that you don't have to use a single global variable named mungo
as in the example above. You could also write your own Singleton with multiple MungoHealer
objects, each with a different ErrorHandler
type. This way you could choose to either show an alert or your custom handling, depending on the context. The Singleton might look something like this:
enum ErrorHandling {
static var alertLogHandler: MungoHealer!
static var myCustomHandler: MungoHealer!
}
Once you've written your own error types and configured your error handler, you should write throwing methods and deal with errors either directly (as before) or use MungoHealer's handle
method which will automatically deal with the error cases.
Here's a throwing method:
private func fetchImage(urlPath: String) throws -> UIImage {
guard let url = URL(string: urlPath) else {
throw StringNotAValidURLFatalError()
}
guard let data = try? Data(contentsOf: url) else {
throw NetworkUnavailableError(retryClosure: { [weak self] intry self?.loadAvatarImage() })
}
guard let image = UIImage(data: data) else {
throw InvalidDataError()
}
return image
}
You can see that different kinds of errors could be thrown here. All of them can be handled at once as easy as this:
private func loadAvatarImage() {
do {
imageView.image = try fetchImage(urlPath: user.avatarUrlPath)
} catch {
mungo.handle(error)
}
}
We don't need to deal with error handling on the call side which makes our code both more readable & more fun to write. Instead, we define how to deal with the errors at the point where the error is thrown/defined. On top of that, the way errors are communicated to the user is abstracted away and can be changed App-wide by simply editing the error handler code. This also makes it possible to handle errors in the model or networking layer without referencing any UIKit
classes.
For cases where you just want one catch-all where you just call the handle(error)
method, there's even a shorthand which will deal with this automatically. Just use this instead of the above code:
private func loadAvatarImage() {
mungo.do {
imageView.image = try fetchImage(urlPath: user.avatarUrlPath)
}
}
So as you can see, used wisely, MungoHealer can help to make your code cleaner, less error prone and it can improve the User Experience for your users.
See the file CONTRIBUTING.md.
This library is released under the MIT License. See LICENSE for details.