Coordinators, Protocol Oriented Programming and MVVM; Bullet-proof architecture with Swift

The term means “Model of a View”, and can be thought of as abstraction of the view, but it also provides a specialization of the Model that the View can use for data-binding. In this latter role the ViewModel contains data-transformers that convert Model types into View types, and it contains Commands the View can use to interact with the Model.

Coordinators completely solve the data flow in the UI layer. They don’t hold nor contain any sort of app’s data — their primary concern is to shuffle data from middleware into front-end UI.

What they do:

Create instances of the VCs

Show or hide VCs

Configure VCs (set DI properties)

plus:

Receive data requests from VC

Route requests to middleware

Route results back to VC

The MVVM / Coordinator Pattern
Flow Diagram for creating this pattern
enum AccountType {
case login, setup, forgotPassword
}
protocol FormTextField {
var apiKey: String { get }
var placeholder: String { get }
var textColor: UIColor { get }
var validationRules: [Rule] { get }
var keyboardType: UIKeyboardType { get }
}
extension FormTextField {
var keyboardType: UIKeyboardType { return .default }
var textColor: UIColor { return UIColor.lightGray }
var validationRules: [Rule] { return [RequiredRule()] }
}
private struct UserNameField: FormTextField {
var placeholder: String = "User Name"
var apiKey: String = "user_name"
}

private struct PasswordField: FormTextField {
var placeholder: String = "Password"
var validationRules: [Rule] = [MinLengthRule(length: 9, message: "Passwords need to be a minimum of 9 characters long")]
var apiKey: String = "password"
}

private struct EmailField: FormTextField {
var placeholder: String = "Email Address"
var validationRules: [Rule] = [EmailRule()]
var apiKey: String = "email_field"
}
typealias FormFields = [FormTextField]

class FormViewModel: NSObject {

private lazy var validator = Validator()
var fields: FormFields {
get {
switch self.type {
case .login:
return [UserNameField(), PasswordField()]

case .setup:
return [EmailField(), UserNameField(), PasswordField()]

case .forgotPassword:
return [EmailField()]
}
}
}

var type: AccountType

init(type: AccountType) {
self.type = type
super.init()
}
func validateForm(){

}
}extension FormViewModel: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

}
}
func setView(for field: FormTextField) -> UITextField {
textField.placeholder = field.placeholder
textField.textColor = field.textColor
return textField
}
extension FormViewModel:  UITableViewDataSource  {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: FormTableViewCell!
cell = tableView.dequeueReusableCell(withIdentifier: FormTableViewCell.reuseID,
for: indexPath) as? FormTableViewCell
?? FormTableViewCell()

let field = cell!.setView(for: fields[indexPath.row])
validator.registerField(field,
rules: fields[indexPath.row].validationRules)
return cell
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fields.count
}
}
datasource = FormViewModel(type: type)
tv.dataSource = self.datasource
typealias ValidationCallback = (Bool, [String]?)

protocol FormViewModelDelegate: class {
func formWasValidated(_ successfully: ValidationCallback)
}
extension FormViewModel: ValidationDelegate {
func validationSuccessful() {
delegate?.formWasValidated((true, nil))
}

func validationFailed(_ errors: [(Validatable, ValidationError)]) {
delegate?.formWasValidated((false, errors.map({ $0.1.errorMessage })))
}
}
func validateForm(){
validator.validate(self)
}
@objc
private func onAcceptButtonTap(){
datasource.validateForm()
}
extension AuthenticateUserViewController: FormViewModelDelegate {
func formWasValidated(_ successfully: ValidationCallback) {
guard let errorMessage = successfully.1 else {
// no validation errors!
return
}

let message = errorMessage.reduce(into: "", { $0 = "\($0 + "\n" + $1)" })
let alertVC = UIAlertController(title: "Form Invalid",
message: message,
preferredStyle: .alert)
alertVC.addAction(UIAlertAction(title: "Rodger!",
style: .destructive,
handler: nil))
present(alertVC, animated: true, completion: nil)
}
}
protocol AuthenticateUserViewControllerDelegate: class {
func userProvidedValidated(credentials: Credentials, type: AccountType)
}
guard let errorMessage = successfully.1 else {
var credentials = Credentials()
for (index, key) in datasource.fields.enumerated() {
guard let cell = form.cellForRow(at: IndexPath(item: index, section: 0)) as? FormTableViewCell else { return }
credentials[key.apiKey] = cell.textField.text
}

delegate?.userProvidedValidated(credentials: credentials, type: datasource.type)
return
}
extension AccountCoordinator: AuthenticateUserViewControllerDelegate {
func userProvidedValidated(credentials: Credentials, type: AccountType) {
dependencies?.networking.perform(call: type, with: credentials, callback: { (response) in
switch response {
case ("success", 200):
print("user logged in!")
break
default:
// issue processing request
break
}
})
}
}
func updateResponder(to type: AccountType)
func updateResponder(to type: AccountType) {
configure(for: type)
}
final class LoginViewController: AuthenticateUserViewController {

lazy var signUpButton: UIButton = self.buttonFactory(with: .signup)

lazy var forgotPassButton: UIButton = self.buttonFactory(with: .forgotpass)


init(){
super.init(type: .login)
}
required init?(coder aDecoder: NSCoder) { fatalError() }

override func loadView() {
super.loadView()
signUpButton.bottomAnchor.constraint(equalTo: acceptButton.topAnchor, constant: -15).isActive = true
signUpButton.leadingAnchor.constraint(equalTo: acceptButton.leadingAnchor).isActive = true
signUpButton.trailingAnchor.constraint(equalTo: acceptButton.trailingAnchor).isActive = true
signUpButton.heightAnchor.constraint(equalToConstant: ButtonViewModel.largeButtonHeight).isActive = true

forgotPassButton.topAnchor.constraint(equalTo: form.bottomAnchor, constant: 15).isActive = true
forgotPassButton.leadingAnchor.constraint(equalTo: acceptButton.leadingAnchor).isActive = true
forgotPassButton.trailingAnchor.constraint(equalTo: acceptButton.trailingAnchor).isActive = true
forgotPassButton.heightAnchor.constraint(equalToConstant: ButtonViewModel.largeButtonHeight).isActive = true
}
}
guard datasource.type != .forgotPassword else {
datasource.validateForm()
return
}
delegate?.updateResponder(to: .forgotPassword)
guard datasource.type != .setup else {
datasource.validateForm()
return
}
delegate?.updateResponder(to: .setup)
private func createViewController(for type: AccountType) -> AuthenticateUserViewController {
guard type != .login else {
return LoginViewController()
}
return AuthenticateUserViewController(type: type)
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store