Skip to content

Instantly share code, notes, and snippets.

@rozd
Created January 16, 2018 09:32
Show Gist options
  • Save rozd/a921a8bb9da0f5eb865c515c8dcc847f to your computer and use it in GitHub Desktop.
Save rozd/a921a8bb9da0f5eb865c515c8dcc847f to your computer and use it in GitHub Desktop.
Invalid Form Error Swift implementation for iOS
//: 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