Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Progressive MVC Architecture

Progressive MVC Architecture

Eeb061c8b2816b771920da1b3e7904a3?s=128

Swift India

November 24, 2018
Tweet

Transcript

  1. Progressive MVC Architecture Rahul Katariya

  2. About Me • iOS Engineer at PhonePe • Creator of

    AarKay.xyz • OpenSource Work • Restofire • SwiftFrameworkTemplate
  3. Apple’s MVC

  4. Model Layer

  5. • SQLite • Core Data • Realm • Network (URLSession)

    • Files • User Defaults • iCloud • Firebase • …..
  6. import UIKit import CoreData class PersonListViewController: UIViewController { var coreDataStack:

    CoreDataStack! var people: [CDPerson] = [] override func viewDidLoad() { super.viewDidLoad() let managedContext = coreDataStack.persistentContainer.viewContext let fetchRequest = NSFetchRequest<CDPerson>(entityName: "Person") do { people = try managedContext.fetch(fetchRequest) } catch let error as NSError { print("Could not fetch. \(error), \(error.userInfo)") } } }
  7. import UIKit import CoreData class AddPersonViewController: UIViewController { var coreDataStack:

    CoreDataStack! @IBAction func addPerson(_ sender: Any) { let name = nameTextField.text!; let age = Int16(ageTextField.text!)!; let managedContext = coreDataStack.persistentContainer.viewContext let person = CDPerson(context: managedContext) person.name = name person.age = age do { try managedContext.save() delegate?.didAddPerson(person) navigationController?.popViewController(animated: true) } catch let error as NSError { print("Could not save. \(error), \(error.userInfo)") } } }
  8. Problems • Tightly Coupled with ViewControllers • Hard to substitute

    with TestData or URL • Hard to test the models
  9. Entities • Introduce Translation Layer • ViewControllers <-> Entities (PONSO)

    <-> Model
  10. import Foundation import CoreData struct Person: Codable { let name:

    String let age: Int16 init(name: String, age: Int16) { self.name = name self.age = age } static func getAll() throws -> [Person] { let managedContext = CoreDataStack.shared.persistentContainer.viewContext let fetchRequest = NSFetchRequest<CDPerson>(entityName: "Person") do { let people = try managedContext.fetch(fetchRequest) return people.map { Person(name: $0.name, age: $0.age) } } catch { fatalError() } } }
  11. Dependency Inversion • Data Providers • Providers <-> ViewControllers <->

    Entities (PONSO)
  12. class CDPersonProvider { let coreDataStack: CoreDataStack init(coreDataStack: CoreDataStack) { self.coreDataStack

    = coreDataStack } func getAll() throws -> [Person] { let managedContext = coreDataStack.persistentContainer.viewContext let fetchRequest = NSFetchRequest<CDPerson>(entityName: "Person") do { let people = try managedContext.fetch(fetchRequest) return people.map { Person(name: $0.name, age: $0.age) } } catch { throw PersonError.fetchError } } }
  13. import UIKit class PersonListViewController: UIViewController { var personService: CDPersonProvider! @IBOutlet

    weak var tableview: PersonListView! override func viewDidLoad() { super.viewDidLoad() title = "People" do { let people = try personService.getAll() tableview.people = people tableview.reloadData() } catch { // Handle error } } }
  14. Liskov Substitution • Introduce Service Protocols • Providers <-> Service

    <-> ViewController <-> Entities
  15. import Foundation protocol PersonService { func getAll() throws -> [Person]

    func createPerson(_ person: Person) throws }
  16. import UIKit class PersonListViewController: UIViewController { var personService: PersonService! @IBOutlet

    weak var tableview: PersonListView! override func viewDidLoad() { super.viewDidLoad() title = "People" do { let people = try personService.getAll() tableview.people = people tableview.reloadData() } catch { // Handle error } } }
  17. class CDPersonProvider: PersonService { let coreDataStack: CoreDataStack init(coreDataStack: CoreDataStack) {

    self.coreDataStack = coreDataStack } func getAll() throws -> [Person] { let managedContext = coreDataStack.persistentContainer.viewContext let fetchRequest = NSFetchRequest<CDPerson>(entityName: "Person") do { let people = try managedContext.fetch(fetchRequest) return people.map { Person(name: $0.name, age: $0.age) } } catch { throw PersonError.fetchError } } }
  18. class LocalPersonProvider: PersonService { } class URLPersonProvider: PersonService { }

    class FirebasePersonProvider: PersonService { }
  19. Test Your Providers!!

  20. View Layer

  21. • Layout subviews • Manage subviews • Data Formatting •

    Event Listening
  22. import UIKit class PersonListViewController: UIViewController { @IBOutlet weak var tableview:

    UITableView! override func viewDidLoad() { super.viewDidLoad() tableview.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") } func tableView( _ tableView: UITableView, numberOfRowsInSection section: Int ) -> Int { return people.count } func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { let person = people[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let text = person.name + " - " + String(person.age) cell.textLabel?.text = text return cell } }
  23. import UIKit class AddPersonViewController: UIViewController { @IBOutlet weak var nameTextField:

    UITextField! @IBOutlet weak var ageTextField: UITextField! @IBOutlet weak var addButton: UIButton! @IBAction func addPerson(_ sender: Any) { let name = nameTextField.text!; let age = Int16(ageTextField.text!)!; let person = Person(name: name, age: age) do { try personService.createPerson(person) delegate?.didAddPerson(person) navigationController?.popViewController(animated: true) } catch { // Handle Error } } }
  24. Problems • Tightly Coupled with ViewControllers • Hard to test

    presentation logic
  25. Single Responsibility • Self contained views • Delegate Pattern

  26. import UIKit protocol AddPersonViewDelegate: class { func didAddPerson(_ person: Person)

    } class AddPersonView: UIView { weak var addPersonViewDelegate: AddPersonViewDelegate? @IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var ageTextField: UITextField! @IBOutlet weak var addButton: UIButton! @IBAction func addPerson(_ sender: Any) { let name = nameTextField.text! let age = Int16(ageTextField.text!)! let person = Person(name: name, age: age) delegate?.didAddPerson(person) } }
  27. import UIKit protocol AddPersonDelegate { func didAddPerson(_ person: Person) }

    class AddPersonViewController: UIViewController, AddPersonViewDelegate { var personService: PersonService! var delegate: AddPersonDelegate? @IBOutlet weak var addPersonView: AddPersonView! override func viewDidLoad() { super.viewDidLoad() title = "Add person" } func didAddPerson(_ person: Person) { do { try personService.createPerson(person) delegate?.didAddPerson(person) navigationController?.popViewController(animated: true) } catch { // Handle Error } } }
  28. ViewModels • Aka Presenters • Data Formatting from Model •

    Transforming User Inputs back to Model • Testable
  29. import Foundation struct AddPersonViewModel { var name: String? var age:

    String? init() {} func person() -> Person? { guard let name = name, let age = age, let numberedAge = Int16(age) else { return nil } return Person(name: name, age: numberedAge) } }
  30. import UIKit protocol AddPersonViewDelegate: class { func didAddPerson(_ person: Person)

    } class AddPersonView: UIView { weak var addPersonViewDelegate: AddPersonViewDelegate? private var addPersonViewModel = AddPersonViewModel() @IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var ageTextField: UITextField! @IBOutlet weak var addButton: UIButton! @IBAction func addPerson(_ sender: Any) { addPersonViewModel.name = nameTextField.text addPersonViewModel.age = ageTextField.text if let person = addPersonViewModel.person() { addPersonViewDelegate?.didAddPerson(person) } else { // Handle validation error } } }
  31. ViewComposition • Reusable Views

  32. import UIKit class AddPersonView: UIView, NameTextFieldDelegate, AgeTextFieldDelegate, AddButtonDelegate { @IBOutlet

    weak var nameTextField: NameTextField! @IBOutlet weak var ageTextField: AgeTextField! @IBOutlet weak var addButton: AddButton! override func awakeFromNib() { super.awakeFromNib() nameTextField.nameTextFieldDelegate = self ageTextField.ageTextFieldDelegate = self addButton.addButtonDelegate = self } }
  33. ViewControllers

  34. • View Lifecycle • Model Interaction • Navigation/Routing

  35. import UIKit class PersonListViewController: UIViewController { var personService: PersonService! override

    func viewDidLoad() { super.viewDidLoad() title = "People" do { let people = try personService.getAll() people.filter { $0.age > 18 } tableview.people = people tableview.reloadData() } catch { // Handle error } } }
  36. Problems • Hard to test business logic • Navigation

  37. Interactors • Aka ViewModel in MVVM • Interacts with data

    providers • Performs business logic • Testable
  38. import Foundation struct PersonListInteractor { let personService: PersonService init(personService: PersonService)

    { self.personService = personService } func getAll() throws -> [Person] { return try personService.getAll() } func getWithAgeGreaterThan(_ age: Int16) throws -> [Person] { let people = try getAll() return people.filter { $0.age > 10 } } }
  39. class PersonListViewController: UIViewController { var personService: PersonService! private var personListInteractor:

    PersonListInteractor! override func viewDidLoad() { super.viewDidLoad() title = "People" personListInteractor = PersonListInteractor(personService: personService) do { let people = try personService.getWithAgeGreaterThan(18) tableview.people = people tableview.reloadData() } catch { // Handle error } } }
  40. Coordinators • Aka Wireframe in Viper • Aka Router in

    RIBs • Navigation
  41. class PersonListViewController: UIViewController { @IBAction func addPersonTapped(_ sender: Any) {

    let storyboard = UIStoryboard(name: "AddPerson", bundle: nil) let vc = storyboard.instantiateInitialViewController() as! AddPersonViewController vc.personService = personService vc.delegate = self navigationController?.pushViewController(vc, animated: true) } }
  42. class AddPersonViewController: UIViewController, AddPersonViewDelegate { func didAddPerson(_ person: Person) {

    do { try personService.createPerson(person) delegate?.didAddPerson(person) navigationController?.popViewController(animated: true) } catch { // Handle Error } } }
  43. protocol PersonListDelegate: class { func didTapAddPerson(_ sender: Any) } class

    PersonListViewController: UIViewController { @IBAction func addPersonTapped(_ sender: Any) { delegate?.didTapAddPerson(sender) } }
  44. • AppDelegate -> PersonListViewController • AppDelegate -> PersonListCoordinator -> PersonListViewController

  45. protocol Coordinator: class { func start() }

  46. import UIKit class PersonListCoordinator: Coordinator, PersonListDelegate { let presenter: UINavigationController

    let personService: PersonService private var personListViewController: PersonListViewController! private let addPersonCoordinator: AddPersonCoordinator init(presenter: UINavigationController, personService: PersonService) { self.presenter = presenter self.personService = personService addPersonCoordinator = AddPersonCoordinator( presenter: presenter, personService: personService ) } func start() { let storyboard = UIStoryboard(name: "PersonList", bundle: nil) personListViewController = storyboard.instantiateInitialViewController() as? PersonListViewController personListViewController.delegate = self personListViewController.personService = personService presenter.pushViewController(personListViewController, animated: true) addPersonCoordinator.delegate = self } func didTapAddPerson(_ sender: Any) { addPersonCoordinator.start() } }
  47. protocol AddPersonDelegate: class { func didAddPerson(_ person: Person) } class

    AddPersonViewController: UIViewController, AddPersonViewDelegate { func didAddPerson(_ person: Person) { do { try personService.createPerson(person) delegate?.didAddPerson(person) } catch { // Handle Error } } }
  48. import UIKit class AddPersonCoordinator: Coordinator, AddPersonDelegate { let presenter: UINavigationController

    let personService: PersonService private var addPersonViewController: AddPersonViewController! init(presenter: UINavigationController, personService: PersonService) { self.presenter = presenter self.personService = personService } func start() { let storyboard = UIStoryboard(name: "AddPerson", bundle: nil) addPersonViewController = storyboard.instantiateInitialViewController() as? AddPersonViewController addPersonViewController.delegate = self addPersonViewController.personService = personService presenter.pushViewController(addPersonViewController, animated: true) } func didAddPerson(_ person: Person) { presenter.popViewController(animated: true) } }
  49. import UIKit protocol AddPersonCoordinatorDelegate: class { func didAddPerson(_ person: Person)

    } class AddPersonCoordinator: Coordinator, AddPersonDelegate { weak var delegate: AddPersonCoordinatorDelegate! func didAddPerson(_ person: Person) { delegate?.didAddPerson(person) presenter.popViewController(animated: true) } }
  50. extension PersonListCoordinator: AddPersonCoordinatorDelegate { func didAddPerson(_ person: Person) { personListViewController.addPerson(person)

    } }
  51. Questions?

  52. References • https://en.wikipedia.org/wiki/SOLID • http://blog.cleancoder.com/uncle-bob/2012/08/13/the- clean-architecture.html • http://www.objc.io/issue-13/viper.html • https://www.raywenderlich.com/158-coordinator-tutorial-

    for-ios-getting-started