-
-
Save hemangshah/77a2f15db01bc9d9e5aa76f60427fc7b to your computer and use it in GitHub Desktop.
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": "[email protected]"])) | |
} | |
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 |
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
}
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?
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)
I would like to see if there's a more better way to handle login and errors handling?