Skip to content

Instantly share code, notes, and snippets.

@hemangshah
Created December 24, 2018 06:51
Show Gist options
  • Select an option

  • Save hemangshah/77a2f15db01bc9d9e5aa76f60427fc7b to your computer and use it in GitHub Desktop.

Select an option

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
Copy Markdown
Author

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

@cenksk
Copy link
Copy Markdown

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
Copy Markdown
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
Copy Markdown

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