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…
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 ofCombine
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
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
adaptingASAuthorizationError
into your own Error gives you a couple of things, more control, the ability to specify thelocalizedDescription
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
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!