Created
January 16, 2018 09:32
-
-
Save rozd/a921a8bb9da0f5eb865c515c8dcc847f to your computer and use it in GitHub Desktop.
Invalid Form Error Swift implementation for iOS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: Playground - noun: a place where people can play | |
import UIKit | |
import PlaygroundSupport | |
// MARK: Transport layer | |
/// Represents error sent from transport layer | |
struct TransportError: Error { | |
let code: Int | |
let data: Any? | |
} | |
/// Some kind of transport. Transport layer is not a part of Form Error and could be various | |
class TransportLayer { | |
/// Sends TransportError with error code 422 (means HTTP code) that will be handled as | |
/// form invalid error. | |
func request(params: [String: Any], completion handler: (Any?, TransportError?) -> ()) { | |
handler(nil, TransportError(code: 422, data: ["password" : "Password is too weak"])) | |
} | |
} | |
// MARK: Infrastructure | |
/// Application service that connects transport layer into application | |
protocol AuthService { | |
func signIn(with form: SignInForm, completion handler: (Bool, SignInForm.Error?) -> ()) | |
} | |
class DefaultAuthService: AuthService { | |
init(transport: TransportLayer) { | |
self.transport = transport | |
} | |
let transport: TransportLayer | |
func signIn(with form: SignInForm, completion handler: (Bool, SignInForm.Error?) -> ()) { | |
transport.request(params: form.asParams()) { data, error in | |
if let error = error { | |
handler(false, FormError<SignInForm>(from: error)) | |
} else { | |
handler(true, nil) | |
} | |
} | |
} | |
} | |
// MARK: Model | |
/// Contract for Form field | |
protocol FormField: RawRepresentable { | |
init?(rawValue: String) | |
} | |
/// Contract for Form | |
protocol Form { | |
associatedtype Field: FormField | |
associatedtype Error = FormError<Self> | |
} | |
/// Form Error | |
enum FormError<ConcreteForm: Form>: Error { | |
typealias InvalidField = (field: ConcreteForm.Field, message: String) | |
case invalid([InvalidField]) | |
case unknown(Error) | |
} | |
/// Converts Transport error into Form Error | |
extension FormError { | |
init(from error: TransportError) { | |
switch error.code { | |
case 422: | |
var results: [InvalidField] = [] | |
if let dict = error.data as? [String: String] { | |
for (fieldName, errorString) in dict { | |
if let field = ConcreteForm.Field(rawValue: fieldName) { | |
results.append((field: field, message: errorString)) | |
} | |
} | |
} | |
self = .invalid(results) | |
default: | |
self = .unknown(error) | |
} | |
} | |
} | |
// MARK: Concrete Error implementation | |
struct SignInForm: Form { | |
let username: String | |
let password: String | |
func asParams() -> [Field.RawValue: Any] { | |
return [Field.username.rawValue: username, | |
Field.password.rawValue: password] | |
} | |
enum Field: String, FormField { | |
case username | |
case password | |
} | |
} | |
// MARK: Usage | |
class ViewController: UIViewController { | |
var service: AuthService! | |
var usernameTextField: UITextField! | |
var passwordTextField: UITextField! | |
override func loadView() { | |
let view = UIView() | |
view.backgroundColor = .white | |
usernameTextField = UITextField(frame: CGRect(x: 40, y: 200, width: 240, height: 44)) | |
usernameTextField.placeholder = "username" | |
view.addSubview(usernameTextField) | |
passwordTextField = UITextField(frame: CGRect(x: 40, y: 252, width: 240, height: 44)) | |
passwordTextField.placeholder = "password" | |
view.addSubview(passwordTextField) | |
let submitButton = UIButton(type: .system) | |
submitButton.frame = CGRect(x: 110, y: 302, width: 100, height: 44) | |
submitButton.setTitle("Submit", for: .normal) | |
submitButton.addTarget(self, action: #selector(ViewController.submitButtonTapped(_:)), for: .touchUpInside) | |
view.addSubview(submitButton) | |
self.view = view | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
service = DefaultAuthService(transport: TransportLayer()) | |
} | |
@objc func submitButtonTapped(_ sender: Any) { | |
guard let username = usernameTextField.text, let password = passwordTextField.text else { | |
let alert = UIAlertController(title: "Error", message: "Please fill username and password fields first.", preferredStyle: .alert) | |
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) | |
present(alert, animated: true, completion: nil) | |
return | |
} | |
service.signIn(with: SignInForm(username: username, password: password)) { success, error in | |
if let error = error { | |
switch error { | |
case .invalid(let fields): | |
for invalid in fields { | |
switch invalid.field { | |
case .username: | |
usernameTextField.text = nil | |
usernameTextField.placeholder = invalid.message | |
case .password: | |
passwordTextField.text = nil | |
passwordTextField.placeholder = invalid.message | |
} | |
} | |
default: | |
print(error.localizedDescription) | |
} | |
} | |
} | |
} | |
} | |
PlaygroundPage.current.liveView = ViewController() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment