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

Flux & MVVM (Brooklyn Swift Developers)

Taiki Suzuki
September 07, 2017

Flux & MVVM (Brooklyn Swift Developers)

Taiki Suzuki

September 07, 2017
Tweet

More Decks by Taiki Suzuki

Other Decks in Programming

Transcript

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

    - How to develop a sample with Flux? - Flux & MVVM
  2. 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.
  3. Dispatcher in FluxCapacitor • FluxCapacitor has single Dispatcher. • Actionable

    and Storable have a DispatchValueType. • The Dispatcher relates Actions to Stores based on DispatchValueTypes.
  4. 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)) } }
  5. Dispatcher.Repository - DispatchValue extension Dispatcher { enum Repository: DispatchValue {

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

    = Dispatcher.Repository // ... private(set) var favorites: [GithubKit.Repository] = [] private(set) var selectedRepository: GithubKit.Repository? = nil // ... }
  7. 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() } } }
  8. 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) } }
  9. subscribe returns a dust, so... Video1 Video1 Robert Zemeckis (1989)

    Back to the future Part II, Universal Pictures
  10. 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) } }
  11. 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" } } }
  12. FavoriteViewController store.subscribe { [weak self] value in DispatchQueue.main.async { switch

    value { case .addFavorite, .removeFavorite: self?.tableView.reloadData() default: break } } } .cleaned(by: dustBuster)
  13. 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? } } }
  14. 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>) { // ... } }
  15. 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)
  16. 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)
  17. 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) } }
  18. 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) } }
  19. 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() } }
  20. 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