Skip to content

Instantly share code, notes, and snippets.

@hemangshah
Created December 24, 2018 06:51
Show Gist options
  • Save hemangshah/77a2f15db01bc9d9e5aa76f60427fc7b to your computer and use it in GitHub Desktop.
Save hemangshah/77a2f15db01bc9d9e5aa76f60427fc7b to your computer and use it in GitHub Desktop.
Trying to implement the best way to handle custom errors through in a login flow (as an example).
import UIKit
// ---- [LoginManager.swift] ---- starts
enum LoginError: Error {
case minUserNameLength(String), minPasswordLength(String), invalidUserName(String), invalidPassword(String)
}
enum LoginResult {
case result([String: Any])
}
struct LoginManager {
static func validate(user: String, pass: String, completion: (LoginResult) -> Void ) throws {
guard (!user.isEmpty && user.count > 8) else { throw LoginError.minUserNameLength("A minimum username length is >= 8 characters.") }
guard (!pass.isEmpty && pass.count > 8) else { throw LoginError.minPasswordLength("A minimum password length is >= 8 characters.") }
//Call Login API to confirm the credentials with provided userName and password values.
//Here we're checking locally for testing purpose.
if user != "iosdevfromthenorthpole" { throw LoginError.invalidUserName("Invalid Username.") }
if pass != "polardbear" { throw LoginError.invalidPassword("Invalid Password.") }
//The actual result will be passed instead of below static result.
completion(LoginResult.result(["userId": 1, "email": "nointernet@thenorthpole.com"]))
}
static func handle(error: LoginError, completion: (_ title: String, _ message: String) -> Void) {
//Note that all associated values are of the same type for the LoginError cases.
//Can we write it in more appropriate way?
let title = "Login failed."
switch error {
case .minUserNameLength(let errorMessage):
completion(title, errorMessage)
case .minPasswordLength(let errorMessage):
completion(title, errorMessage)
case .invalidUserName(let errorMessage):
completion(title, errorMessage)
case .invalidPassword(let errorMessage):
completion(title, errorMessage)
}
}
}
// ---- [LoginManager.swift] ---- ends
// ---- [LoginViewController.swift] ---- starts
//Confirming the user credentials when user taps on the "Login" button.
do {
try LoginManager.validate(user: "iosdevfromthenorthpole", pass: "polardbear", completion: { (loginResult) in
switch loginResult {
case .result (let result):
print("userId: ", result["userId"] ?? "Not available.")
print("email: ", result["email"] ?? "Not available.")
}
})
} catch let error as LoginError {
LoginManager.handle(error: error) { (title, message) in
//Show an alert with title and message to the user.
print(title + " " + message)
}
}
// ---- [LoginViewController.swift] ---- ends
@hemangshah
Copy link
Author

I would like to see if there's a more better way to handle login and errors handling?

@cenksk
Copy link

cenksk commented Dec 24, 2018

Maybe it will be like this also,

enum MembershipValidationResult: Error, CustomStringConvertible {
    case minUserNameLength(String), minPasswordLength(String), invalidUserName(String), invalidPassword(String), success(String,String), unknownError(String)
    
    var description: String {
        switch self {
        case .minUserNameLength(let message):
            return message
        case .minPasswordLength(let message):
            return message
        case .invalidUserName(let message):
            return message
        case .invalidPassword(let message):
            return message
        case .success(let s1, let s2):
            return "\(s1 + s2)"
        case .unknownError(let message):
            return message
        }
    }
}


protocol Validation {
    func validate() -> MembershipValidationResult
}

class MembershipManager {
    private var errorStrategy: Validation?
    
    func validate(es: Validation) throws {
        errorStrategy = es
        throw errorStrategy?.validate() ?? .unknownError("Somthing went wrong")
    }
}

class LoginValidation: Validation {
    var userName: String
    var password: String
    
    init(userName: String, password: String) {
        self.userName = userName
        self.password = password
    }
    
    func validate() -> MembershipValidationResult {
        guard (!userName.isEmpty && userName.count > 8) else { return MembershipValidationResult.minUserNameLength("A minimum username length is >= 8 characters.") }
        guard (!password.isEmpty && password.count > 8) else { return MembershipValidationResult.minPasswordLength("A minimum password length is >= 8 characters.") }
        if userName != "iosdevfromthenorthpole" { return MembershipValidationResult.invalidUserName("Invalid Username.") }
        if userName != "polardbear" { return MembershipValidationResult.invalidPassword("Invalid Password.") }
        return .success(userName, password)
    }
}

let manager = MembershipManager()

do {
    try manager.validate(es: LoginValidation(userName: "iosdevfromthenorthpole", password: "polardbear"))
} catch let error as MembershipValidationResult {
    error.description
}

@hemangshah
Copy link
Author

Hi, thanks for the review and added your code! This also looks awesome however not sure, which one is the better approach? From your code, what I understood is that I can use the Validation protocol to validates multiple objects of different types for example: Login, Registration, Feedback, Payment etc. Just everywhere where I need to use the validation. The only change I will need to do is to add more cases here in MembershipValidationResult enum. So this is cool, but if you noticed that in my code (in LoginViewController.swift view-controller class), while validating the login credentials, I am also ended-up with a completion block which will tell me whether the login is successful or not, how to implement the same thing in your code, while making it generic for multiple cases?

@ARamy23
Copy link

ARamy23 commented Dec 31, 2018

Hi,I usually use 2 enums to keep things simple when testing this scenario, so i'd love to share it and would love if someone would give a feedback on it too,
1st, i validate the required fields that we gather from the UI to send to our backend and some other validations too through a custom validator class

first i'll declare an enum of Validations Error

enum ValidationError: Error {
    case unreachable
    case serverIsDown
    case invalidAPIKey
    case genericError
    case emptyValue(key: String)
    
    var message: String {
        switch self {
        case .unreachable: return "No Internet Connection, Please try again later"
        case .invalidAPIKey: return "Invalid API Key"
        case .serverIsDown: return "Server is currently down, Please try again later"
        case .genericError: return "Oops... Something went wrong"
        case .emptyValue(key: let key): return "Please fill in the \(key) value" //this one can be very useful in fields validations
        }
    }
}

then I declare a protocol called validator which has only one function that throws errors

protocol Validator {
    func orThrow() throws // strange name ik, but u gonna like it later on :)
}

and we can implement it in a class like this

typealias ToSeeIfIsReachable = ReachabilityValidator // yep ik ik, strange name again but bare with me :D
class ReachabilityValidator: Validator {
   func orThrow() {
       guard !Reachability.isConnectedToNetwork() else { return }
       throw ValidationError.unreachable
   }
}

or a class for let's say an email field like this

typealias ToSeeIfIsNotEmpty = EmptyValueValidator

class EmptyValueValidator: Validator {
    var value: Any?
    var key: String
    
    init(value: Any?, key: String) {
        self.value = value
        self.key = key
    }
    
    func orThrow() throws {
        switch value {
        case "" as String:
            throw ValidationError.emptyValue(key: key) // ;) 
        case nil:
            throw ValidationError.emptyValue(key: key) // ;)
        default: break
        }
    }
}

so in my interactor, viewModel, worker whichever layer u do ur logic in
i have 3 functions that i use to handle any logic and i override them in the children of my BaseInteractor that looks like this

class BaseInteractor {
    var service: BaseService.Type?
    
    init(service: BaseService.Type?) {
        self.service = service
    }
    
    func toValidate() throws { // read down below to know why i picked this name for the function :D 
        try ToSeeIfIsReachable().orThrow() // now u know why i was using that strange typealias for the ReachabilityValidator ;)
    }
    
    func request(onComplete: @escaping (CodableInit?, Error?) -> Void) {}
    
    func performARequest(onComplete: @escaping (CodableInit?, Error?) -> Void) {
        do {
            try toValidate() // more readable ig?
            request(onComplete: onComplete)
        } catch let error {
            onComplete(nil, error)
        }
    }
}

then if am validating the input of login i do it this way

class LoginInteractor: BaseInteractor {
    var email, password: String?
    
    init(email: String?, password: String?) {
        self.email = email
        self.password = password
    }
    
    override func toValidate() throws {
        try super.toValidate()
        try ToSeeIfIsNotEmpty(value: email, key: Keys.loginEmailField).orThrow()
        try ToSeeIfIsNotEmpty(value: password, key: Keys.loginPasswordField).orThrow()
    }
}

override func request(onCompletion: @escaping (Codable?, Error) { 
super.request(onCompletion)
// do ur networking here or logic here
}

then when am using this class
i just call loginInteractor.performARequest()
when i do this line, the validations occurs and if there is something that occurred and failed in my validations, it wouldn't go in the catch block and then i can show the user the errorMessage directly in a popup or an alert

so, what if the BE returns his own custom error?

i usually use this approach when creating an enum out of a response JSON which contains an error message or an error flag or an error case (ex.: BRANCH_NOT_AVAILABLE_NOW)

enum LoginError: Error {
    case branchNotAvailableNow
    case genericError
    
    init(rawValue: String) {  // u can use any type that u can then map an enum case out of it, for my example i'll use a string
        switch rawValue {
        case "BRANCH_NOT_AVAILABLE_NOW": self = .branchNotAvailableNow
        default: self = .genericError
        }
    }
}

then i make an extension (or just write the extension in the enum declaration, however u like)

extension LoginError {
    var errorMessage: String {
        switch self {
        case . branchNotAvailableNow: return "branchNotAvailableNow".localized
        case .genericError: return "genericError".localized
        }
    }
}

so those are my two approaches that i use mainly when doing validations or building an errorMessage out of a response, sometimes not
anyways, id really love to hear someone's feedback on this approach (also, i got a feedback from a senior friend of mine that this was an overkill, but IMO i think this is just a separation of concerns to make life stupid simple when testing my code)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment