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

Transcript

  1. 2.

    Overview - Self-introduction - A Sample App for Today's Subject

    - How to develop a sample with Flux? - Flux & MVVM
  2. 3.
  3. 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.
  4. 6.
  5. 10.
  6. 11.
  7. 12.
  8. 16.
  9. 18.
  10. 19.

    Dispatcher in FluxCapacitor • FluxCapacitor has single Dispatcher. • Actionable

    and Storable have a DispatchValueType. • The Dispatcher relates Actions to Stores based on DispatchValueTypes.
  11. 20.
  12. 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)) } }
  13. 22.
  14. 23.

    Dispatcher.Repository - DispatchValue extension Dispatcher { enum Repository: DispatchValue {

    case addFavorite(GithubKit.Repository) case removeFavorite(GithubKit.Repository) case selectedRepository(GithubKit.Repository?) } }
  15. 24.
  16. 25.

    RepositoryStore - Storable final class RepositoryStore: Storable { typealias DispatchValueType

    = Dispatcher.Repository // ... private(set) var favorites: [GithubKit.Repository] = [] private(set) var selectedRepository: GithubKit.Repository? = nil // ... }
  17. 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() } } }
  18. 27.
  19. 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) } }
  20. 29.

    subscribe returns a dust, so... Video1 Video1 Robert Zemeckis (1989)

    Back to the future Part II, Universal Pictures
  21. 30.
  22. 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) } }
  23. 32.
  24. 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" } } }
  25. 34.
  26. 35.

    FavoriteViewController store.subscribe { [weak self] value in DispatchQueue.main.async { switch

    value { case .addFavorite, .removeFavorite: self?.tableView.reloadData() default: break } } } .cleaned(by: dustBuster)
  27. 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? } } }
  28. 39.
  29. 40.
  30. 41.
  31. 42.
  32. 44.
  33. 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>) { // ... } }
  34. 46.
  35. 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)
  36. 48.
  37. 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)
  38. 50.
  39. 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) } }
  40. 52.
  41. 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) } }
  42. 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() } }
  43. 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