Error Handling: Wrapping

Error Handling: Wrapping

Hello, hello! 🙌

Been a long time, I know (guilty as charged), it’s been a rough month, and I barely had time to set things up, so today (thank god) is the day we finally continue talking about Error Handling!

TL;DR

You can directly go to this branch and just read the code from there

Plan setting time

Let’s first agree that 3rd party modules are not always controllable, so to get over this, we wrap it into our wrappers and build upon these wrappers, and in some cases, we map it into our implementations.

💡 You may find wrappers are sort of like Facades, but we don’t want to use confusing smart words now do we? (Actually, we do, so you might wanna google what a Facade is in case you don’t 😁)

So… tenor.gif

What are we building today?

Today, we’re building a simple Sign In with Apple app

But! before we start there are a couple of disclaimers

  • UI was mainly built by DSKit, I haven’t done much except play around with it
  • No actual Sign-in takes place, so don’t expect that we’re going to authenticate you somewhere, or do anything with your credentials 😊

The Problem

So, why the Sign In with Apple?

It has lots of potential for today’s topic, where you may find yourself kinda coupled to putting its logic inside the ViewController or View just because it needs a presentationContextProvider or the delegate

So why not wrap that delegate inside our workflow?

💡 For this post (probably like every other post), we're going to use MVVM, but this time we're going to utilize a bit of Combine

Wrapping Time

Defining our Wrapper (Facade)

Since all we want from Apple Sign-in is getting to know when it succeeded, and when it failed, along with the success response, and the error resulted, for the sake of this post, I'll be interested in the error only (well, since it's a topic around Error Handling, so... yep 🤷‍♂️)

So, let's create our API contract!

public protocol AppleAuthUIDelegateProtocol: ASAuthorizationControllerDelegate {
  var onSuccess: VoidCallback { get set }
  var onError: Callback<ASAuthorizationError> { get set }
}

💡 Whatever comes after this is just a conformer, so I can fake this to drive something I want to see/test specifically

💡 You may notice that we mentioned that we're wrapping the UIDelegate and called it a Facade, so what's going on?

As it seems that we're doing 3 different things, but they're all the same

  • We're wrapping Apple's Delegate for the Apple Sign In
  • We're facade-ing it, so it's simpler
  • And the delegate hasn't changed, so nothing changed here
public final class AppleAuthUIDelegate: NSObject, AppleAuthUIDelegateProtocol {
  public var onSuccess: VoidCallback
  public var onError: Callback<ASAuthorizationError>

  public init(onSuccess: @escaping VoidCallback, onError: @escaping Callback<ASAuthorizationError>) {
    self.onSuccess = onSuccess
    self.onError = onError
  }

  public func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization _: ASAuthorization) {
    onSuccess()
  }

  public func authorizationController(controller _: ASAuthorizationController, didCompleteWithError error: Error) {
    guard let error = error as? ASAuthorizationError else {
      onError(.init(.unknown))
      return
    }

    onError(error)
  }
}

Reusing Apple Sign-In's Presentation Logic

Sure, we can put all that logic inside a LoginViewModel, no problem, but what if you wanted to reuse it, and export it into its own module, segregating sure does like a good idea

class AppleSignInViewModel {
  ...
  // 👇 UI Delegate added with a couple of handlers
  private lazy var uiDelegate: AppleAuthUIDelegateProtocol = AppleAuthUIDelegate(
    onSuccess: onSuccessHandler,
    onError: onErrorHandler)

  private lazy var onSuccessHandler: VoidCallback = { [weak self] in
    guard let self = self else { return }
    ...
  }

  private lazy var onErrorHandler: Callback<ASAuthorizationError> = { [weak self] error in
    guard let self = self else { return }
    ...
  }
 ...
}

So, now that we have our ui delegate & a couple of handlers to catch the delegate methods results, we can start defining how we're going to handle the errors

let's quickly define some struct that represents our UI state for AppleSignIn, so far, its easy enough to wrap inside an enum, so let's do so

extension AppleSignInViewModel {
  enum State {
    case idle
    case success
    case failure(errorMessage: String)
  }
}

Now back to the onErrorHandler so it can propagate the failure state to the LoginViewModel and then to the View

...
  private lazy var onErrorHandler: Callback<ASAuthorizationError> = { [weak self] error in
    guard let self = self else { return }
    self.state = .failure(errorMessage: error.localizedDescription)
  }
...

Now, let's try our Error Pipeline

CleanShot 2022-08-21 at 06.28.00.gif

And Voil..., wait a minute... why does localizedDescription look so cryptic?

Defining our own Error

You may have noticed that Apple sends ASAuthorizationError, and its .localizedDescription is not very, um... localized? but also, to our own code, it's not a 1st party error, and controlling it is a bit tricky if dealt with directly, so defining it in our own context is a good idea

🤔 Apple's own error not being a 1st party error? does he realize that we're building on their own platform?

💡 Yep, while you're indeed building above Apple's Foundation & UIKit, but not above AuthenticationServices adapting ASAuthorizationError into your own Error gives you a couple of things, more control, the ability to specify the localizedDescription

So, let's define our AppleSignInError

public struct AppleSignInError: PresentationError {
  public let title: String
  public let description: String?
  public let type: PresentationMethod
  public let icon: UIImage?

  public init(title: String, description: String? = nil, type: PresentationMethod = .indicator, icon: UIImage? = nil) {
    self.title = title
    self.description = description
    self.type = type
    self.icon = icon
  }
}

👀 We will talk more about PresentationError in a later post and what role it plays

Now that we have our AppleSignInError, let's add some adaption code from ASAuthorizationError -> AppleSignInError

extension AppleSignInError {
  init(_ authError: ASAuthorizationError) {
    switch authError.code {
    case .unknown:
      self = .init(title: "Something went wrong", description: "with an unknown reason")
    default:
      self = .init(title: "Sign in failed")
    }
  }
}

And now we can ignore firing an error from the ViewModel if it doesn't make sense from a presentation POV, like a canceled error for example 😄

private lazy var onErrorHandler: Callback<ASAuthorizationError> = { [weak self] error in
    guard let self = self else { return }
    guard error.code != .canceled else { return }
    self.state = .failure(.init(error))
  }

Wrapping it up

Now, if we ran our project again and tried to cancel, we no longer get an error when we cancel the sign-in process using Apple

CleanShot 2022-08-21 at 08.38.50.gif

Conclusion

We've cut a lot of ground today, not only did we discuss Facading and wrapping 3rd party code into our own code, but we set a strong foundation for the coming post which is going to take a good turn in defining Error Handling responsibility among different layers in Clean Architecture

You can follow the project here on GitHub

Aaand, It's a wrap!