MVC→MVP→MVVM→Fluxの実装の違いを比較してみる

Ae276805027a01983503c3edafbdb6b2?s=47 Taiki Suzuki
September 17, 2017

 MVC→MVP→MVVM→Fluxの実装の違いを比較してみる

iOSDC 2017 9/17 13:50 TrackB
https://iosdc.jp/2017/node/1396

iOSDesignPatternSamples
https://github.com/marty-suzuki/iOSDesignPatternSamples

FluxCapacitory
https://github.com/marty-suzuki/FluxCapacitor

Flux & MVVM (Brooklyn Swift Developers)
https://speakerdeck.com/martysuzuki/flux-and-mvvm-brooklyn-swift-developers

補足資料
【iOSDC2017】MVC→MVP→MVVM→Fluxの実装の違いを比較してみる
https://qiita.com/marty-suzuki/items/5a4f680b10bb82501aa3

Ae276805027a01983503c3edafbdb6b2?s=128

Taiki Suzuki

September 17, 2017
Tweet

Transcript

  1. MVP←MVC ↳MVVM→Flux ͷ࣮૷ͷҧ͍Λൺֱͯ͠ΈΔ iOSDC 2017: September 17th Taiki Suzuki /

    @marty_suzuki
  2. ΞδΣϯμ - ࣗݾ঺հ - MVC - MVC→MVP - MVP→MVVM -

    MVVM→Flux
  3. None
  4. None
  5. Github Sample App

  6. None
  7. MVC

  8. None
  9. final class FavoriteViewController: UIViewController, UITableViewDelegate { @IBOutlet weak var tableView:

    UITableView! ... override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self ... } ... private func showRepository(with repository: Repository) { let vc = RepositoryViewController(repository: repository, favoriteModel: favoriteModel) navigationController?.pushViewController(vc, animated: true) } ... func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let repository = favoriteModel.favorites[indexPath.row] showRepository(with: repository) } }
  10. final class RepositoryViewController: SFSafariViewController { ... private let repository: Repository

    private let favoriteModel: FavoriteModel init(repository: Repository, favoriteModel: FavoriteModel) { self.repository = repository self.favoriteModel = favoriteModel super.init(url: repository.url, entersReaderIfAvailable: true) } ... }
  11. final class FavoriteModel { private(set) var favorites: [Repository] = []

    { didSet { delegate?.favoriteDidChange?() } } weak var delegate: FavoriteModelDelegate? func addFavorite(_ repository: Repository) { favorites.append(repository) } func removeFavorite(_ repository: Repository) { favorites.remove(at: index) } }
  12. @objc protocol FavoriteModelDelegate: class { @objc optional func favoriteDidChange() }

    final class FavoriteViewController: UIViewController, FavoriteModelDelegate { @IBOutlet weak var tableView: UITableView! let favoriteModel = FavoriteModel() ... override func viewDidLoad() { super.viewDidLoad() ... favoriteModel.delegate = self ... } ... func favoriteDidChange() { tableView.reloadData() } }
  13. final class RepositoryViewController: SFSafariViewController { private let favoriteModel: FavoriteModel private

    let repository: Repository private(set) lazy var favoriteButtonItem: UIBarButtonItem ... @objc private func favoriteButtonTap(_ sender: UIBarButtonItem) { if favoriteModel.favorites.index(where: { $0.url == repository.url }) == nil { favoriteModel.addFavorite(repository) ... } else { favoriteModel.removeFavorite(repository) ... } } }
  14. None
  15. None
  16. None
  17. protocol FavoriteView: class { ... func showRepository(with repository: Repository) }

    final class FavoriteViewController: UIViewController, FavoriteView { @IBOutlet weak var tableView: UITableView! private(set) lazy var presenter: FavoritePresenter = FavoriteViewPresenter(view: self) ... func showRepository(with repository: Repository) { let vc = RepositoryViewController(repository: repository, favoritePresenter: presenter) navigationController?.pushViewController(vc, animated: true) } }
  18. final class RepositoryViewController: SFSafariViewController { ... private let presenter: RepositoryPresenter

    init(repository: Repository, favoritePresenter: FavoritePresenter) { self.presenter = .init(repository: repository, favoritePresenter: favoritePresenter) super.init(url: repository.url, entersReaderIfAvailable: true) } ... }
  19. protocol FavoritePresenter: class { ... func addFavorite(_ repository: Repository) func

    removeFavorite(_ repository: Repository) func contains(_ repository: Repository) -> Bool }
  20. final class FavoriteViewPresenter: FavoritePresenter { private weak var view: FavoriteView?

    private var favorites: [Repository] = [] { didSet { view?.reloadData() } } ... func addFavorite(_ repository: Repository) { if contains(repository) { return } favorites.append(repository) } func removeFavorite(_ repository: Repository) { guard let index = index(of: repository) else { return } favorites.remove(at: index) } private func index(of repository: Repository) -> Int? { return favorites.lazy.index { $0.url == repository.url } } func contains(_ repository: Repository) -> Bool { return index(of: repository) != nil } }
  21. protocol FavoriteView: class { func reloadData() ... } final class

    FavoriteViewController: UIViewController, FavoriteView { @IBOutlet weak var tableView: UITableView! private(set) lazy var presenter: FavoritePresenter = .init(view: self) ... func reloadData() { tableView?.reloadData() } }
  22. final class RepositoryViewController: SFSafariViewController { private(set) lazy var favoriteButtonItem: UIBarButtonItem

    ... private let presenter: RepositoryPresenter init(repository: Repository, favoritePresenter: FavoritePresenter, entersReaderIfAvailable: Bool = true) { self.presenter = .init(repository: repository, favoritePresenter: favoritePresenter) super.init(url: repository.url, entersReaderIfAvailable: entersReaderIfAvailable) self.presenter.view = self } ... @objc private func favoriteButtonTap(_ sender: UIBarButtonItem) { presenter.favoriteButtonTap() } }
  23. protocol RepositoryPresenter: class { init(repository: Repository, favoritePresenter: FavoritePresenter) weak var

    view: RepositoryView? { get set } ... func favoriteButtonTap() }
  24. final class RepositoryViewPresenter: RepositoryPresenter { weak var view: RepositoryView? private

    let favoritePresenter: FavoritePresenter private let repository: Repository ... init(repository: Repository, favoritePresenter: FavoritePresenter) { self.repository = repository self.favoritePresenter = favoritePresenter } ... func favoriteButtonTap() { if favoritePresenter.contains(repository) { favoritePresenter.removeFavorite(repository) ... } else { favoritePresenter.addFavorite(repository) ... } } }
  25. None
  26. - ViewͱPresenterͰ੹຿Λ੾Γ෼͚ Δ͜ͱ͕Ͱ͖Δ! - ViewΛMockԽ͢Δ͜ͱͰPresenter ͷςετ͕༰қʹͰ͖Δ! - ந৅Խ͍ͯ͠Δ͕ɺPresenterͱ View͸ޓ͍ʹґଘ͍ͯ͠Δ!

  27. None
  28. None
  29. Data binding

  30. final class FavoriteViewModel { let relaodData: Observable<Void> ... private let

    _favorites = Variable<[Repository]>([]) private let disposeBag = DisposeBag() ... init(favoritesObservable: Observable<[Repository]>, ...) { ... self.relaodData = _favorites.asObservable().map { _ in } favoritesObservable.bind(to: _favorites).disposed(by: disposeBag) } ... }
  31. final class FavoriteViewController: UIViewController { ... var favoritesInput: AnyObserver<[Repository]> {

    return favorites.asObserver() } var favoritesOutput: Observable<[Repository]> { return viewModel.favorites } private let favorites = PublishSubject<[Repository]>() private let disposeBag = DisposeBag() private private(set) lazy var viewModel: FavoriteViewModel = { .init(favoritesObservable: self.favorites, ...) }() ... private var showRepository: AnyObserver<Repository> { return UIBindingObserver(UIElement: self) { me, repository in let vc = RepositoryViewController(repository: repository, favoritesOutput: me.favoritesOutput, favoritesInput: me.favoritesInput) me.navigationController?.pushViewController(vc, animated: true) }.asObserver() } ... }
  32. final class RepositoryViewController: SFSafariViewController { private let favoriteButtonItem: UIBarButtonItem private

    let viewModel: RepositoryViewModel ... init(repository: Repository, favoritesOutput: Observable<[Repository]>, favoritesInput: AnyObserver<[Repository]>) { let favoriteButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil) self.favoriteButtonItem = favoriteButtonItem self.viewModel = RepositoryViewModel(repository: repository, favoritesOutput: favoritesOutput, favoritesInput: favoritesInput, favoriteButtonTap: favoriteButtonItem.rx.tap) super.init(url: repository.url, entersReaderIfAvailable: true) } ... }
  33. final class RepositoryViewModel { private let disposeBag = DisposeBag() ...

    init(repository: Repository, favoritesOutput: Observable<[Repository]>, favoritesInput: AnyObserver<[Repository]>, favoriteButtonTap: ControlEvent<Void>) { let favoritesAndIndex: Observable<([Favorites], Int?)> = favoritesOutput .map { [repository] favorites in (favorites, favorites.index(where: { $0.url == repository.url })) } ... favoriteButtonTap .withLatestFrom(favoritesAndIndex) .map { [repository] favorites, index in var favorites = favorites if let index = index { favorites.remove(at: index) return favorites } favorites.append(repository) return favorites } .subscribe(onNext: { favoritesInput.onNext($0) }).disposed(by: disposeBag) } }
  34. final class FavoriteViewController: UIViewController { @IBOutlet weak var tableView: UITableView!

    ... private let disposeBag = DisposeBag() private private(set) lazy var viewModel: FavoriteViewModel ... override func viewDidLoad() { super.viewDidLoad() ... viewModel.relaodData.bind(to: reloadData).disposed(by: disposeBag) } ... private var reloadData: AnyObserver<Void> { return UIBindingObserver(UIElement: self) { me, _ in me.tableView.reloadData() }.asObserver() } }
  35. None
  36. - ObservableΛ֎෦͔Β஫ೖ͢Δ͜ͱ ʹΑͬͯλοϓͷςετͳͲ΋Մೳ! - ViewController͸ViewModelʹґଘ ͍ͯ͠Δ͕ɺViewModel͸͠ͳ͍! - ඪ४ϑϨʔϜϫʔΫ͚ͩͰ͸ɺ࣮૷ ͕͠ʹ͍͘!

  37. None
  38. None
  39. ActionɾDispatcherɾStore ͷఆٛ

  40. final class RepositoryAction: Actionable { typealias DispatchValueType = Dispatcher.Repository ...

    func selectRepository(_ repository: Repository) { invoke(.selectedRepository(repository)) } ... } extension Dispatcher { enum Repository: DispatchValue { case selectedRepository(GithubKit.Repository?) ... } }
  41. final class RepositoryStore: Storable { typealias DispatchValueType = Dispatcher.Repository ...

    let selectedRepository: Observable<Repository?> var selectedRepositoryValue: Repository? { return _selectedRepository.value } private let _selectedRepository = Variable<Repository?>(nil) ... init(dispatcher: Dispatcher) { self.selectedRepository = _selectedRepository.asObservable() ... register { [weak self] in switch $0 { case .selectedRepository(let value): self?._selectedRepository.value = value ... } } } }
  42. final class FavoriteViewController: UIViewController { ... private let store: RepositoryStore

    = .instantiate() private let action = RepositoryAction() ... private var showRepository: AnyObserver<Void> { return UIBindingObserver(UIElement: self) { me, _ in guard let vc = RepositoryViewController() else { return } me.navigationController?.pushViewController(vc, animated: true) }.asObserver() } ... }
  43. final class RepositoryViewController: SFSafariViewController { private let action: RepositoryAction private

    let store: RepositoryStore ... init?() { self.store = .instantiate() self.action = .init() guard let repository = store.selectedRepositoryValue else { return nil } super.init(url: repository.url, entersReaderIfAvailable: true) } override func viewDidLoad() { super.viewDidLoad() let repository = store.selectedRepository.filter { $0 != nil }.map { $0! } ... } ... }
  44. final class RepositoryAction: Actionable { typealias DispatchValueType = Dispatcher.Repository ...

    func clearSelectedRepository() { invoke(.selectedRepository(nil)) } func addFavorite(_ repository: Repository) { invoke(.addFavorite(repository)) } func removeFavorite(_ repository: Repository) { invoke(.removeFavorite(repository)) } } extension Dispatcher { enum Repository: DispatchValue { ... case addFavorite(GithubKit.Repository) case removeFavorite(GithubKit.Repository) case removeAllFavorites } }
  45. final class RepositoryStore: Storable { typealias DispatchValueType = Dispatcher.Repository ...

    let favorites: Observable<[Repository]> var favoritesValue: [Repository] { return _favorites.value } private let _favorites = Variable<[Repository]>([]) ... init(dispatcher: Dispatcher) { self.favorites = _favorites.asObservable() ... register { [weak self] in switch $0 { ... case .addFavorite(let value): if self?._favorites.value.index(where: { $0.url == value.url }) == nil { self?._favorites.value.append(value) } case .removeFavorite(let value): if let index = self?._favorites.value.index(where: { $0.url == value.url }) { self?._favorites.value.remove(at: index) } case .removeAllFavorites: self?._favorites.value.removeAll() } } } }
  46. final class RepositoryViewController: SFSafariViewController { private let favoriteButtonItem = .init(title:

    nil, style: .plain, target: nil, action: nil) private let disposeBag = DisposeBag() private let action: RepositoryAction private let store: RepositoryStore ... override func viewDidLoad() { super.viewDidLoad() let repository = store.selectedRepository.filter { $0 != nil }.map { $0! } let containsRepository = Observable.combineLatest(repository, store.favorites) { repo, favs in (favs.contains { $0.url == repo.url }, repo) } favoriteButtonItem.rx.tap .withLatestFrom(containsRepository) .subscribe(onNext: { [weak self] contains, repository in if contains { self?.action.removeFavorite(repository) } else { self?.action.addFavorite(repository) } }) .disposed(by: disposeBag) } ... }
  47. final class FavoriteViewController: UIViewController { @IBOutlet weak var tableView: UITableView!

    private let dataSource = FavoriteViewDataSource() private let disposeBag = DisposeBag() private let store: RepositoryStore = .instantiate() private let action = RepositoryAction() override func viewDidLoad() { super.viewDidLoad() ... store.favorites.map { _ in } .bind(to: reloadData) .disposed(by: disposeBag) } ... private var reloadData: AnyObserver<Void> { return UIBindingObserver(UIElement: self) { me, _ in me.tableView.reloadData() }.asObserver() } }
  48. None
  49. - σʔλϑϩʔ͕୯Ұํ޲! - ભҠ࣌ʹΦϒδΣΫτͷࢀরΛ ViewControllerʹ౉͢ඞཁ͕ͳ͍! - Dispatcher͕γϯάϧτϯʢ࣮૷ʹ Αͬͯ͸Store΋ʣ!

  50. https:/ /github.com/marty-suzuki/ iOSDesignPatternSamples

  51. None
  52. https:/ /speakerdeck.com/martysuzuki/flux-and- mvvm-brooklyn-swift-developers

  53. Flux + MVVM Sample with FluxCapacitor https:/ /github.com/marty-suzuki/FluxCapacitor/ tree/master/Examples/Flux%2BMVVM

  54. ͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠! Taiki Suzuki Twitter: @marty_suzuki Github: marty-suzuki