$30 off During Our Annual Pro Sale. View Details »

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

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

Taiki Suzuki

September 17, 2017
Tweet

More Decks by Taiki Suzuki

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. View Slide

  4. View Slide

  5. Github Sample App

    View Slide

  6. View Slide

  7. MVC

    View Slide

  8. View Slide

  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)
    }
    }

    View Slide

  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)
    }
    ...
    }

    View Slide

  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)
    }
    }

    View Slide

  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()
    }
    }

    View Slide

  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)
    ...
    }
    }
    }

    View Slide

  14. View Slide

  15. View Slide

  16. View Slide

  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)
    }
    }

    View Slide

  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)
    }
    ...
    }

    View Slide

  19. protocol FavoritePresenter: class {
    ...
    func addFavorite(_ repository: Repository)
    func removeFavorite(_ repository: Repository)
    func contains(_ repository: Repository) -> Bool
    }

    View Slide

  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
    }
    }

    View Slide

  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()
    }
    }

    View Slide

  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()
    }
    }

    View Slide

  23. protocol RepositoryPresenter: class {
    init(repository: Repository, favoritePresenter: FavoritePresenter)
    weak var view: RepositoryView? { get set }
    ...
    func favoriteButtonTap()
    }

    View Slide

  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)
    ...
    }
    }
    }

    View Slide

  25. View Slide

  26. - ViewͱPresenterͰ੹຿Λ੾Γ෼͚
    Δ͜ͱ͕Ͱ͖Δ!
    - ViewΛMockԽ͢Δ͜ͱͰPresenter
    ͷςετ͕༰қʹͰ͖Δ!
    - ந৅Խ͍ͯ͠Δ͕ɺPresenterͱ
    View͸ޓ͍ʹґଘ͍ͯ͠Δ!

    View Slide

  27. View Slide

  28. View Slide

  29. Data binding

    View Slide

  30. final class FavoriteViewModel {
    let relaodData: Observable
    ...
    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)
    }
    ...
    }

    View Slide

  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 {
    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()
    }
    ...
    }

    View Slide

  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)
    }
    ...
    }

    View Slide

  33. final class RepositoryViewModel {
    private let disposeBag = DisposeBag()
    ...
    init(repository: Repository,
    favoritesOutput: Observable<[Repository]>,
    favoritesInput: AnyObserver<[Repository]>,
    favoriteButtonTap: ControlEvent) {
    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)
    }
    }

    View Slide

  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 {
    return UIBindingObserver(UIElement: self) { me, _ in
    me.tableView.reloadData()
    }.asObserver()
    }
    }

    View Slide

  35. View Slide

  36. - ObservableΛ֎෦͔Β஫ೖ͢Δ͜ͱ
    ʹΑͬͯλοϓͷςετͳͲ΋Մೳ!
    - ViewController͸ViewModelʹґଘ
    ͍ͯ͠Δ͕ɺViewModel͸͠ͳ͍!
    - ඪ४ϑϨʔϜϫʔΫ͚ͩͰ͸ɺ࣮૷
    ͕͠ʹ͍͘!

    View Slide

  37. View Slide

  38. View Slide

  39. ActionɾDispatcherɾStore ͷఆٛ

    View Slide

  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?)
    ...
    }
    }

    View Slide

  41. final class RepositoryStore: Storable {
    typealias DispatchValueType = Dispatcher.Repository
    ...
    let selectedRepository: Observable
    var selectedRepositoryValue: Repository? { return _selectedRepository.value }
    private let _selectedRepository = Variable(nil)
    ...
    init(dispatcher: Dispatcher) {
    self.selectedRepository = _selectedRepository.asObservable()
    ...
    register { [weak self] in
    switch $0 {
    case .selectedRepository(let value):
    self?._selectedRepository.value = value
    ...
    }
    }
    }
    }

    View Slide

  42. final class FavoriteViewController: UIViewController {
    ...
    private let store: RepositoryStore = .instantiate()
    private let action = RepositoryAction()
    ...
    private var showRepository: AnyObserver {
    return UIBindingObserver(UIElement: self) { me, _ in
    guard let vc = RepositoryViewController() else { return }
    me.navigationController?.pushViewController(vc, animated: true)
    }.asObserver()
    }
    ...
    }

    View Slide

  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! }
    ...
    }
    ...
    }

    View Slide

  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
    }
    }

    View Slide

  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()
    }
    }
    }
    }

    View Slide

  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)
    }
    ...
    }

    View Slide

  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 {
    return UIBindingObserver(UIElement: self) { me, _ in
    me.tableView.reloadData()
    }.asObserver()
    }
    }

    View Slide

  48. View Slide

  49. - σʔλϑϩʔ͕୯Ұํ޲!
    - ભҠ࣌ʹΦϒδΣΫτͷࢀরΛ
    ViewControllerʹ౉͢ඞཁ͕ͳ͍!
    - Dispatcher͕γϯάϧτϯʢ࣮૷ʹ
    Αͬͯ͸Store΋ʣ!

    View Slide

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

    View Slide

  51. View Slide

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

    View Slide

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

    View Slide

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

    View Slide