Skip to content

Latest commit

 

History

History
264 lines (174 loc) · 8.81 KB

README.md

File metadata and controls

264 lines (174 loc) · 8.81 KB

SwiftErrorHandler

Swift 5.0 Travis Status Maintainability CocoaPods Version Badge Carthage compatible License Badge Platform

SwiftErrorHandler enables expressing complex error handling logic with a few lines of code using a memorable fluent API.

Installation

CocoaPods

pod 'SwiftErrorHandler', '~> 5.0'

Carthage

github "stefanrenne/SwiftErrorHandler" ~> 5.0

Swift Package Manager (SPM)

import PackageDescription

let package = Package(
  name: "My App",
    dependencies: [
      .package(url: "https://github.com/stefanrenne/SwiftErrorHandler.git", from: "5.0.0")
    ]
)

Usage

Let's say we're building a account based iOS app that can throw errors in the networking layer.

We need to:

Setup a default ErrorHandler once

The default ErrorHandler will contain the error handling logic that is common across your application that you don't want to duplicate. You can create a factory that creates it so that you can get new instance with common handling logic from anywhere in your app.

extension ErrorHandler {
  class func `default`(for view: ErrorHandlerView) -> ErrorHandler {
    return ErrorHandler(for: view)
      .on(error: .code(NSURLErrorTimedOut), then: .present(alert: ConfirmableAlert(title: "Timeout occurred", confirmTitle: "Retry", confirmAction: { error in print("retry network call") })))
      .on(error: .type(NetworkError.noInternet), then: .present(alert: ConfirmableAlert(title: "Did you turn off the internet?", confirmTitle: "No")))
      .on(error: .type(NetworkError.logout), then: .present(alert: RejectableAlert(title: "Are you sure you want to logout?", confirmTitle: "Yes", rejectTitle: "No")))
      .always(.perform(action: AnalyticsService.track))
      .onNoMatch(.present(alert: ConfirmableAlert(title: "Something went wrong", confirmTitle: "Ok")))
    }
}

Use the default handler to handle common cases

Often the cases the default handler knows about will be good enough.

do {
  try saveStatus()
} catch {
  ErrorHandler.default(for: self).handle(error: error)
}

Customize the error handler when needed.

In cases where extra context is available you can add more cases or override the ones provided already.

For example in a LoginViewController

class LoginViewController: UIViewController {
    
  private lazy var errorHandler = ErrorHandler.default(for: self)
    .on(error: .type(NetworkError.authenticate), then: .perform(action: startAuthentication))
        
  func performLogin() {
    do {
      try login()
    } catch {
      errorHandler.handle(error: error)
    }
  }
    
  private func startAuthentication(for error: Error, onCompleted: OnErrorHandled) {
    print("start authentication ...")
    onCompleted?()
    return true
  }      
}

Bonus: RxSwift Support

let errorHandler = ErrorHandler.default(for: self)
Observable<User>
  .error(NetworkError.authenticate)
  .subscribe(onNext: { result in
      print("User loggedin")
    },
    onError: errorHandler.handle)
  .disposed(by: disposeBag)

Bonus: Result Support

let errorHandler = ErrorHandler.default(for: self)
let result: Result<User, NetworkError> = .failure(NetworkError.authenticate)
let user: User? = result.get(onError: errorHandler)

Customization options

The way actions are performed for errors

  • Performs actions for specific errors

    errorHandler.on(error: .code(404), then: .present(Alert))

  • Performs actions when no specific error matcher can be found

    errorHandler.onNoMatch(.present(Alert))

  • Actions that need to be performed for all errors

    errorHandler.always(.perform(action: analyticsService.track))

Error Matchers

Match on specific error type

errorHandler.on(error: .type(NetworkError.authenticate), then: .doNothing)

Match on NSError code

``errorHandler.on(error: .code(404), then: .doNothing)`

Match on NSError domain

errorHandler.on(error: .domain("remote"), then: .doNothing)

Custom matching

extension ErrorMatcher {
    static func onCustomMatch() -> ErrorMatcher {
        .init(matcher: { error in 
            ...
            return true 
        })
    }
}

.on(error: .onCustomMatch()), then: .doNothing)

Error Handling

Do nothing

It mainly exists to make documentation & unit tests easier to understand.

errorHandler.on(error: .code(404), then: .doNothing)

Present Alert

The Alert is presented on the View provided in the ErrorHandler init

errorHandler.on(error: .code(404), then: .present(alert: ErrorAlert))

By default there are two alert types you can present:

  • ConfirmableAlert: An alert with one action button
  • RejectableAlert: An alert with two action buttons

Would you like to use different alerts?

  1. Create a struct that conforms to the ErrorAlert protocol
  2. Implement the function that builds your custom UIAlertController func build(for error: Error, onCompleted: OnErrorHandled) -> UIAlertController
  3. Make sure the optional onCompleted completionblock has been performed in all UIAlertAction completion blocks

Custom Action

The only limitation is your mind.

errorHandler.on(error: .code(404), then: .perform(action: CustomActionHandler)

The CustomActionHandler provides the Error and an optional onCompleted completionblock that needs to be executed when your custom action has been performed.

Implementing the ErrorHandler outside the ViewController

In larger apps it makes sense to implement the ErrorHandler in a different class than the ViewController. To make this work you need to provide a view on which alerts can be presented. This can be done by conforming to the ErrorHandlerView protocol.

public protocol ErrorHandlerView {
  func present(alert: UIAlertController)
}

extension UIViewController: ErrorHandlerView {
  public func present(alert: UIAlertController) {
    present(alert, animated: true, completion: nil)
  }
}

Contribute?

Build your xcode project using the Swift Package Manager

swift package generate-xcodeproj --xcconfig-overrides ./Sources/ios.xcconfig

Quick checklist summary before submitting a PR

  • 🔎 Make sure tests are added or updated to accomodate your changes. We do not accept any addition that comes without tests. When possible, add tests to verify bug fixes and prevent future regressions.
  • 📖 Check that you provided a CHANGELOG entry documenting your changes (except for documentation improvements)
  • 👌 Verify that tests pass
  • 👍 Push it!

Why?

When designing for errors, we usually need to:

  1. have a default handler for expected errors // i.e. network, db errors etc.
  2. handle specific errors in a custom manner given the context of where and when they occur // i.e. a network error while uploading a file, invalid login
  3. have unspecific handlers that get executed on every error // i.e. log errors to Fabric or any other analytics service
  4. have a catch-all handler for unknown errors // i.e. errors we don't have custom handling for
  5. keep our code DRY

Swift has a well thought error handling model that balances between convenience (automatic propagation) and clarity-safety (Typed propagation, Marked propagation). As a result, the compiler warns of errors that need to be handled, while making it relatively easy to propagate errors and handle them higher up the stack.

However, even with this help from the language, achieving the goals listed above in an ad-hoc manner in an application of a reasonable size can lead to a lot of boilerplate which is tedious to write and reason about. Because of this friction, developers quite often choose to swallow errors or handle them all in the same generic way.

This library addresses these issues by providing an abstraction to define flexible error handling rules with an opinionated, fluent API.