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

今更聞けないMVPとMVVMの違い

 今更聞けないMVPとMVVMの違い

イベントページ
【サポーターズCoLab勉強会】【iOS】今更聞けないMVPとMVVMの違い
https://supporterzcolab.com/event/339/
サンプルソース
https://github.com/marty-suzuki/DiffMVPAndMVVM
https://github.com/marty-suzuki/SimplestCounterSample

Taiki Suzuki

April 05, 2018
Tweet

More Decks by Taiki Suzuki

Other Decks in Programming

Transcript

  1. MVP

  2. final class MVPViewController: UIViewController, CounterView { @IBOutlet private(set) var labels:

    [UILabel]! @IBOutlet private(set) weak var incrementButton: UIButton! @IBOutlet private(set) weak var upButton: UIButton! @IBOutlet private(set) weak var downButton: UIButton! private lazy var presenter = CounterPresenter(numberOfDigits: self.labels.count, view: self) override func viewDidLoad() { super.viewDidLoad() incrementButton.addTarget(presenter, action: #selector(CounterPresenter.incrementButtonTap), for: .touchUpInside) upButton.addTarget(presenter, action: #selector(CounterPresenter.upButtonTap), for: .touchUpInside) downButton.addTarget(presenter, action: #selector(CounterPresenter.downButtonTap), for: .touchUpInside) } func updateLabel(at index: Int, text: String) { labels[index].text = text } }
  3. final class CounterPresenter { private weak var view: CounterView? ...

    // other properties init(numberOfDigits: Int, view: CounterView) { self.view = view ... // other implementations } @objc func incrementButtonTap() { ... // increment implementations } @objc func upButtonTap() { ... // up implementations } @objc func downButtonTap() { ... // down implementations } }
  4. final class CounterViewMock: CounterView { struct UpdateParameters { let index:

    Int let text: String } private let didCallUpdateLabel: (UpdateParameters) -> () init(didCallUpdateLabel: @escaping (UpdateParameters) -> ()) { self.didCallUpdateLabel = didCallUpdateLabel } func updateLabel(at index: Int, text: String) { didCallUpdateLabel(UpdateParameters(index: index, text: text)) } }
  5. final class CounterPresenterTestCase: XCTestCase { private var counterView: CounterViewMock! private

    var presetner: CounterPresenter! private var results: [CounterViewMock.UpdateParameters]! override func setUp() { super.setUp() self.results = [] let counterView = CounterViewMock() { [weak self] result in self?.results.append(result) } self.presetner = CounterPresenter(numberOfDigits: 4, view: counterView) self.counterView = counterView } ... // other implementations }
  6. final class CounterPresenterTestCase: XCTestCase { ... // other implementations func

    testInitialValue() { XCTAssertEqual(results.count, 4) results.forEach { result in XCTAssertEqual(result.text, "0") } } func testIncrementButtonTap() { XCTAssertEqual(results.count, 4) presetner.incrementButtonTap() XCTAssertEqual(results.count, 8) XCTAssertEqual(results[4].text, "1") XCTAssertEqual(results[5].text, "0") XCTAssertEqual(results[6].text, "0") XCTAssertEqual(results[7].text, "0") } }
  7. Depends on ... final class MVPViewController: UIViewController, CounterView { ...

    // other properties private lazy var presenter = CounterPresenter(numberOfDigits: self.labels.count, view: self) ... // other implementations }
  8. @objc protocol CounterPresenterType: class { init(numberOfDigits: Int, view: CounterView) @objc

    func incrementButtonTap() @objc func upButtonTap() @objc func downButtonTap() } final class CounterPresenter: CounterPresenterType { ... // other implementations }
  9. final class MVPViewController<Presenter: CounterPresenterType>: UIViewController, CounterView { ... // other

    properties private lazy var presenter = Presenter(numberOfDigits: self.labels.count, view: self) init() { super.init(nibName: "MVPViewController", bundle: nil) } ... // other implementations }
  10. final class CounterPresenterMock: CounterPresenterType { let numberOfDigits: Int private(set) weak

    var view: CounterView? private(set) var incrementButtonTapCount: Int = 0 private(set) var upButtonTapCount: Int = 0 private(set) var downButtonTapCount: Int = 0 init(numberOfDigits: Int, view: CounterView) { self.numberOfDigits = numberOfDigits self.view = view } @objc func incrementButtonTap() { incrementButtonTapCount += 1 } @objc func upButtonTap() { upButtonTapCount += 1 } @objc func downButtonTap() { downButtonTapCount += 1 } }
  11. final class MVPViewControllerTestCase: XCTestCase { private var viewController: MVPViewController! private

    var presenter: CounterPresenterMock! override func setUp() { super.setUp() let viewController = MVPViewController<CounterPresenterMock>() _ = viewController.view self.viewController = viewController self.presenter = viewController.presenter } ... // other implementations }
  12. final class MVPViewControllerTestCase: XCTestCase { // other implementations func testNumberOfPlaceValues()

    { XCTAssertEqual(viewController.labels.count, presenter.numberOfDigits) } func testUpdateLabelAtIndex0To1() { let index: Int = 0 XCTAssertNotNil(viewController.labels[index].text) XCTAssertNotEqual(viewController.labels[index].text, "1") viewController.updateLabel(at: index, text: "1") XCTAssertEqual(viewController.labels[index].text, "1") } func testIncrementButtonTap() { XCTAssertEqual(presenter.incrementButtonTapCount, 0) viewController.incrementButton.sendActions(for: .touchUpInside) XCTAssertEqual(presenter.incrementButtonTapCount, 1) } }
  13. final class MVVMViewController: UIViewController { @IBOutlet private(set) var labels: [UILabel]!

    @IBOutlet private(set) weak var incrementButton: UIButton! @IBOutlet private(set) weak var upButton: UIButton! @IBOutlet private(set) weak var downButton: UIButton! private let disposeBag = DisposeBag() private lazy var viewModel: CounterViewModel = { .init(numberOfDigits: self.labels.count, incrementButtonTap: self.incrementButton.rx.tap.asObservable(), upButtonTap: self.upButton.rx.tap.asObservable(), downButtonTap: self.downButton.rx.tap.asObservable()) }() override func viewDidLoad() { super.viewDidLoad() viewModel.placeValues .bind(to: Binder(self) { me, values in values.enumerated().forEach { me.labels[$0].text = $1 } }) .disposed(by: disposeBag) } }
  14. final class CounterViewModel { let placeValues: Observable<[String]> private let disposeBag

    = DisposeBag() ... // other properties init(numberOfDigits: Int, incrementButtonTap: Observable<Void>, upButtonTap: Observable<Void>, downButtonTap: Observable<Void>) { ... // other implementations incrementButtonTap .subscribe(onNext: { ... // increment implementations }) .disposed(by: disposeBag) upButtonTap .subscribe(onNext: { ... // up implementations }) .disposed(by: disposeBag) downButtonTap .subscribe(onNext: { ... // down implementations }) .disposed(by: disposeBag) } }
  15. final class CounterViewModelTestCase: XCTestCase { private var viewModel: CounterViewModel! private

    var incrementButtonTap: PublishRelay<Void>! private var upButtonTap: PublishRelay<Void>! private var downButtonTap: PublishRelay<Void>! private var disposeBag: DisposeBag! private var results: BehaviorRelay<[String]>! override func setUp() { super.setUp() let incrementButtonTap = PublishRelay<Void>() let upButtonTap = PublishRelay<Void>() let downButtonTap = PublishRelay<Void>() self.viewModel = CounterViewModel(numberOfDigits: 4, incrementButtonTap: incrementButtonTap.asObservable(), upButtonTap: upButtonTap.asObservable(), downButtonTap: downButtonTap.asObservable()) self.incrementButtonTap = incrementButtonTap self.upButtonTap = upButtonTap self.downButtonTap = downButtonTap let disposeBag = DisposeBag() self.disposeBag = disposeBag let results = BehaviorRelay<[String]>(value: []) viewModel.placeValues .bind(to: results) .disposed(by: disposeBag) self.results = results } ... // other implementations }
  16. final class CounterViewModelTestCase: XCTestCase { ... // other implementations func

    testInitialValue() { XCTAssertEqual(results.value.count, 4) results.value.forEach { value in XCTAssertEqual(value, "0") } } func testIncrementButtonTap() { let lastValue = results.value XCTAssertEqual(lastValue.count, 4) incrementButtonTap.accept(()) XCTAssertNotEqual(lastValue, results.value) XCTAssertEqual(results.value.count, 4) XCTAssertEqual(results.value[0], "1") XCTAssertEqual(results.value[1], "0") XCTAssertEqual(results.value[2], "0") XCTAssertEqual(results.value[3], "0") } }
  17. Depends on ... final class MVVMViewController: UIViewController { ... //

    other properties private lazy var viewModel: CounterViewModel = { .init(numberOfDigits: self.labels.count, incrementButtonTap: self.incrementButton.rx.tap.asObservable(), upButtonTap: self.upButton.rx.tap.asObservable(), downButtonTap: self.downButton.rx.tap.asObservable()) }() ... // other implementations }
  18. protocol CounterViewModelType: class { var placeValues: Observable<[String]> { get }

    init(numberOfDigits: Int, incrementButtonTap: Observable<Void>, upButtonTap: Observable<Void>, downButtonTap: Observable<Void>) } final class CounterViewModel: CounterViewModelType { ... // other implementations }
  19. final class MVVMViewController<ViewModel: CounterViewModelType>: UIViewController { ... // other properties

    private lazy var viewModel: ViewModel = { .init(numberOfDigits: self.labels.count, incrementButtonTap: self.incrementButton.rx.tap.asObservable(), upButtonTap: self.upButton.rx.tap.asObservable(), downButtonTap: self.downButton.rx.tap.asObservable()) }() init() { super.init(nibName: "MVVMViewController", bundle: nil) } ... // other implementations }
  20. final class CounterViewModelMock: CounterViewModelType { let placeValues: Observable<[String]> let _placeValues

    = PublishRelay<[String]>() let numberOfDigits: Int private(set) var incrementButtonTapCount: Int = 0 private(set) var upButtonTapCount: Int = 0 private(set) var downButtonTapCount: Int = 0 private let disposeBag = DisposeBag() init(numberOfDigits: Int, incrementButtonTap: Observable<Void>, upButtonTap: Observable<Void>, downButtonTap: Observable<Void>) { self.placeValues = _placeValues.asObservable() self.numberOfDigits = numberOfDigits incrementButtonTap .subscribe(onNext: { [weak self] in self?.incrementButtonTapCount += 1 }) .disposed(by: disposeBag) ... // other implementations } }
  21. final class MVVMViewControllerTestCase: XCTestCase { private var viewController: MVVMViewController! private

    var viewModel: CounterViewModelMock! private var placeValues: PublishRelay<[String]>! override func setUp() { super.setUp() let viewController = MVVMViewController<CounterViewModelMock>() _ = viewController.view self.viewController = viewController self.viewModel = viewController.viewModel self.placeValues = viewController.viewModel._placeValues } ... // other implementations }
  22. final class MVVMViewControllerTestCase: XCTestCase { ... // other implementations func

    testNumberOfPlaceValues() { XCTAssertEqual(viewController.labels.count, viewModel.numberOfDigits) } func testUpdateLabelAtIndex0To1() { let index: Int = 0 XCTAssertNotNil(viewController.labels[index].text) XCTAssertNotEqual(viewController.labels[index].text, "1") placeValues.accept(["1", "0", "0", "0"]) XCTAssertEqual(viewController.labels[index].text, "1") } func testIncrementButtonTap() { XCTAssertEqual(viewModel.incrementButtonTapCount, 0) viewController.incrementButton.sendActions(for: .touchUpInside) XCTAssertEqual(viewModel.incrementButtonTapCount, 1) } }
  23. final class MVVMSampleViewController: UIViewController { @IBOutlet private weak var incrementButton:

    UIButton! @IBOutlet private weak var decrementButton: UIButton! @IBOutlet private weak var countLabel: UILabel! private let viewModel = CountViewModel() override func viewDidLoad() { super.viewDidLoad() incrementButton.addTarget(viewModel, action: #selector(CountViewModel.increment), for: .touchUpInside) decrementButton.addTarget(viewModel, action: #selector(CountViewModel.decrement), for: .touchUpInside) viewModel.observe(keyPath: \.count, bindTo: countLabel, \.text) ... // other implementations } }
  24. final class CountViewModel { private enum Names { static let

    countChanged = Notification.Name(rawValue: "CountViewModel.countChanged") ... // other static properties } ... // other properties private var observers: [NSObjectProtocol] = [] private let center: NotificationCenter init(center: NotificationCenter = .init()) { self.center = center ... // other implementations } ... // other implementations }
  25. final class CountViewModel { ... // other properties private(set) var

    count: String = "" { didSet { center.post(name: Names.countChanged, object: nil) } } private var _count: Int = 0 { didSet { count = String(_count) ... // other implementations } } ... // other implementations @objc func increment() { _count += 1 } @objc func decrement() { _count -= 1 } }
  26. final class CountViewModel { ... // other implementations func observe<Value1,

    Target: AnyObject, Value2>(keyPath keyPath1: KeyPath<CountViewModel, Value1>, bindTo target: Target, _ keyPath2: ReferenceWritableKeyPath<Target, Value2>) { let name: Notification.Name switch keyPath1 { case \CountViewModel.count: name = Names.countChanged ... // other cases } let handler: () -> () = { [weak self, weak target] in guard let me = self, let target = target, let value = me[keyPath: keyPath1] as? Value2 else { return } target[keyPath: keyPath2] = value } handler() observers.append(center.addObserver(forName: name, object: nil, queue: .main) { _ in handler() }) } }
  27. 1. Interfaces are different. - A Presenter has a reference

    of View, and calls methods of View. - A ViewModel publishes changes with callbacks and so on, and a View observes them. 2. Logics can be almost same.