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

Flux & MVVM (Brooklyn Swift Developers)

Ae276805027a01983503c3edafbdb6b2?s=47 Taiki Suzuki
September 07, 2017

Flux & MVVM (Brooklyn Swift Developers)

Ae276805027a01983503c3edafbdb6b2?s=128

Taiki Suzuki

September 07, 2017
Tweet

More Decks by Taiki Suzuki

Other Decks in Programming

Transcript

  1. Flux & MVVM Brooklyn Swift Developers September 7th, 2017 Taiki

    Suzuki / @marty_suzuki
  2. Overview - Self-introduction - A Sample App for Today's Subject

    - How to develop a sample with Flux? - Flux & MVVM
  3. None
  4. Photo with Doc!

  5. AbemaTV - A Internet TV station - 24 Live Streaming

    Channels - Video On Demand Contents - 20 million1 downloads in Japan2 2 Only available in Japan and contains regions checking 1 Incidentally, the Japanese population is 127 million.
  6. None
  7. AbemaTV Based on the Flux design pattern

  8. What design patterns have you used in your project?

  9. A Sample App for Today's Subject

  10. None
  11. In MVC...

  12. None
  13. I have a FluxFigure1 Figure1 PIKOTARO (2016) https:/ /www.youtube.com/watch?v=0E00Zuayv9Q

  14. How to develop this sample with Flux?

  15. Flux Figure2 Figure2 Facebook (2014) Flux, http:/ /facebook.github.io/flux/docs/in-depth-overview.html#content

  16. None
  17. FluxCapacitor is used in this example https:/ /github.com/marty-suzuki/FluxCapacitor

  18. FluxCapacitor provides protocols to make implementing the Flux design pattern

    easily. - Actionable - Storable - DispatchValue
  19. Dispatcher in FluxCapacitor • FluxCapacitor has single Dispatcher. • Actionable

    and Storable have a DispatchValueType. • The Dispatcher relates Actions to Stores based on DispatchValueTypes.
  20. None
  21. RepositoryAction - Actionable final class RepositoryAction: Actionable { typealias DispatchValueType

    = Dispatcher.Repository // ... func selectedRepository(_ repository: GithubKit.Repository) { invoke(.selectedRepository(repository)) } func removeFavorite(_ repository: GithubKit.Repository) { invoke(.removeFavorite(repository)) } func addFavorite(_ respository: GithubKit.Repository) { invoke(.addFavorite(respository)) } }
  22. None
  23. Dispatcher.Repository - DispatchValue extension Dispatcher { enum Repository: DispatchValue {

    case addFavorite(GithubKit.Repository) case removeFavorite(GithubKit.Repository) case selectedRepository(GithubKit.Repository?) } }
  24. None
  25. RepositoryStore - Storable final class RepositoryStore: Storable { typealias DispatchValueType

    = Dispatcher.Repository // ... private(set) var favorites: [GithubKit.Repository] = [] private(set) var selectedRepository: GithubKit.Repository? = nil // ... }
  26. RepositoryStore - Storable init(dispatcher: Dispatcher) { register { [weak self]

    dispatchValue in switch dispatchValue { case .addFavorite(let value): if self?.favorites.index(where: { $0.url == value.url }) == nil { self?.favorites.append(value) } case .removeFavorite(let value): if let index = self?.favorites.index(where: { $0.url == value.url }) { self?.favorites.remove(at: index) } case .removeAllFavorites: self?.favorites.removeAll() } } }
  27. None
  28. final class FavoriteViewController: UIViewController { private let action = RepositoryAction()

    private let store = RepositoryStore.instantiate() private let dustBuster = DustBuster() override func viewDidLoad() { super.viewDidLoad() store.subscribe { [weak self] value in DispatchQueue.main.async { switch value { case .addFavorite, .removeFavorite: self?.tableView.reloadData() default: break } } }.cleaned(by: dustBuster) } }
  29. subscribe returns a dust, so... Video1 Video1 Robert Zemeckis (1989)

    Back to the future Part II, Universal Pictures
  30. None
  31. final class RepositoryViewController: SFSafariViewController { private let action = RepositoryAction()

    private let store: RepositoryStore private let repository: Repository // ... init?(store: RepositoryStore = .instantiate()) { guard let repository = store.selectedRepository else { return nil } self.store = store self.repository = repository super.init(url: repository.url, entersReaderIfAvailable: true) } }
  32. None
  33. extension RepositoryViewController { @objc fileprivate func favoriteButtonTap(_ sender: UIBarButtonItem) {

    if store.favorites.contains(where: { $0.url == repository.url }) { action.removeFavorite(repository) favoriteButtonItem.title = "Add" } else { action.addFavorite(repository) favoriteButtonItem.title = "Remove" } } }
  34. None
  35. FavoriteViewController store.subscribe { [weak self] value in DispatchQueue.main.async { switch

    value { case .addFavorite, .removeFavorite: self?.tableView.reloadData() default: break } } } .cleaned(by: dustBuster)
  36. Thinking about testing extension RepositoryViewController { @objc fileprivate func favoriteButtonTap(_

    sender: UIBarButtonItem) { if store.favorites.contains(where: { $0.url == repository.url }) { action.removeFavorite(repository) favoriteButtonItem.title = "Add" // <- how to test? } else { action.addFavorite(repository) favoriteButtonItem.title = "Remove" // <- how to test? } } }
  37. Figure1 Figure1 PIKOTARO (2016) https:/ /www.youtube.com/watch?v=0E00Zuayv9Q

  38. Model-View-ViewModel Figure3 Figure3 https:/ /en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel

  39. None
  40. None
  41. None
  42. None
  43. RxSwift https:/ /github.com/ReactiveX/RxSwift

  44. None
  45. RepositoryViewModel final class RepositoryViewModel { private let action: RepositoryAction private

    let store: RepositoryStore private let disposeBag = DisposeBag() let buttonTitle: Observable<String> private let _buttonTitle = BehaviorSubject<String>(value: "") init(action: RepositoryAction = .init(), store: RepositoryStore = .instantiate(), favoriteButtonItemTap: ControlEvent<Void>) { // ... } }
  46. None
  47. RepositoryViewModel - init let selectedRepository = store.selectedRepository // Observable<Repository?> .filter

    { $0 != nil }.map { $0! } favoriteButtonItemTap .withLatestFrom(selectedRepository) .subscribe(onNext: { [weak self] repository in guard let me = self else { return } if me.store.favoritesValue.contains(where: { $0.url == repository.url }) { me.action.removeFavorite(repository) } else { me.action.addFavorite(repository) } }).disposed(by: disposeBag)
  48. None
  49. RepositoryViewModel - init let selectedRepository = store.selectedRepository .filter { $0

    != nil }.map { $0! } store.favorites // Observable<[Repository]> .withLatestFrom(selectedRepository) { $0 } .map { favorites, repository in let contains = favorites.contains { $0.url == repository.url } return contains ? "Remove" : "Add" } .bind(to: _buttonTitle).disposed(by: disposeBag)
  50. None
  51. RepositoryViewController final class RepositoryViewController: SFSafariViewController { private let favoriteButtonItem =

    UIBarButtonItem() private let disposeBag = DisposeBag() private(set) lazy var viewModel: RepositoryViewModel = { return .init(favoriteButtonItemTap: self.favoriteButtonItem.rx.tap) }() override func viewDidLoad() { super.viewDidLoad() viewModel.buttonTitle .bind(to: favoriteButtonItem.rx.title) .disposed(by: disposeBag) } }
  52. None
  53. RepositoryViewModelTestCase class RepositoryViewModelTestCase: XCTestCase { var action: RepositoryAction! var store:

    RepositoryStore! var viewModel: RepositoryViewModel! var favoriteButtonItemTap = PublishSubject<Void>() override func setUp() { super.setUp() self.action = RepositoryAction() self.store = RepositoryStore.instantiate() let controlEvent = ControlEvent(events: favoriteButtonItemTap) self.viewModel = RepositoryViewModel(action: action, store: store, favoriteButtonItemTap: controlEvent) } }
  54. extension RepositoryViewModelTestCase { func testButtonTitle() { // set favorite repository

    as selectedRepository let repository = Repository.mock() action.selectedRepository(repository) action.addFavorite(repository) let expectation = self.expectation(description: "testButtonTitle expectation") let disposable = viewModel.buttonTitle .skip(1) .subscribe(onNext: { title in XCTAssertEqual(title, "Add") expectation2.fulfill() }) favoriteButtonItemTap.onNext() // turn "Remove" to "Add" waitForExpectations(timeout: 1, handler: nil) disposable.dispose() } }
  55. Flux and MVVM combination can clearly separate a presentation logic

    and testable more than Flux only.
  56. Flux Sample with FluxCapacitor https:/ /github.com/marty-suzuki/FluxCapacitor/ tree/master/Examples/Flux

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

  58. It is said "Circulate the Flux" by our team members

    when to use the Flux update cycle. Figure1 Figure1 PIKOTARO (2016) https:/ /www.youtube.com/watch?v=0E00Zuayv9Q
  59. In the FluxCapacitor case... Video2 Video2 Robert Zemeckis (1985) Back

    to the future, Universal Pictures
  60. Thank you for listening and enjoy Fluxing ! https:/ /github.com/marty-suzuki/

    FluxCapacitor