Abema iOS Architecture

3925ee6eaa41031bac799de0f4f528ec?s=47 to4iki
April 25, 2019

Abema iOS Architecture

3925ee6eaa41031bac799de0f4f528ec?s=128

to4iki

April 25, 2019
Tweet

Transcript

  1. 16.
  2. 17.

    Flux Data in a Flux application flows in a single

    direction 2 2 https://facebook.github.io/flux/docs/in-depth-overview.html 17
  3. 19.

    Dispatcher ActionType ͷ୅ΘΓʹઐ༻ͷ DispatchSubject Λෳ਺༻ҙ ※ DispatchSubject: PublishSubject ͷϥούʔ final

    class Dispatcher { static let shared = Dispatcher() let someModel = DispatchSubject<SomeModel> let isLoading = DispatchSubject<Bool> let error = DispatchSubject<Error> } 19
  4. 21.

    Action • Clean Architecture3 ͷ֎ԁɺ Devices, Web, DB, UIΛActionͱ͢Δ 3

    https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean- architecture.html 21
  5. 22.

    Action e.g. APIΛୟ࣮͘૷ func someAction() { dispatcher.isLoading.dispatch(true) api.getSome() .do(onError: {

    [weak self] error in self?.dispatcher.error.dispatch(error)ɹ// Τϥʔ }) .do(onCompleted: { [weak self] in self?.dispatcher.isLoading.dispatch(false)ɹ// ׬ྃ }) .subscribe(onNext: { [weak self] model in self?.dispatcher.someModel.dispatch(model)ɹ// ੒ޭ }) .disposed(by: disposeBag) } 22
  6. 24.

    Store • ঢ়ଶ: Property<State> • ίϚϯυ: Observable<Event> class Store {

    /// state let isLoading: Property<Bool> /// command let showErrorView: Observable<Void> init(dispatcher: Dispatcher = .shared) { // dispatcher͔ΒྲྀΕ͖ͯͨΠϕϯτΛbind͢Δ } } 24
  7. 25.

    Rx.Peoperty? A get-only BehaviorRelay that is (almost) equivalent to ReactiveSwift's

    Property. 4 set ΍ bind ͕ग़དྷͳ͍ BehaviorRelay class Store { let value: Property<Int> private let _value = BehaviorRelay<Int>(value: 0) init() { self.value = Property(_value) } } store.value.accept(1) // error 4 https://github.com/inamiy/RxProperty 25
  8. 27.
  9. 30.

    ViewStream InputͷΠϕϯτετϦʔϜΛݩʹOutputͷετϦʔϜΛੜ੒͢Δ final class ViewStream { // output let state:

    Property<T> let event: Observable<T> // input init(viewDidAppear: Observable<Void>, didTapButton: Observable<Void>) { // inputͷΠϕϯτετϦʔϜΛ΋ͱʹoutputΛੜ੒͢Δ } } 30
  10. 31.

    MVVM Pros/Cons Pros • ঢ়ଶͷϥΠϑαΠΫϧ؅ཧָ͕ • ViewStream ͷ deinit Ͱࣗಈॲཧ

    • ୯ମςετָ͕ Cons • ϝοηʔδϯά͕ϦϨʔͩΒ͚ʹͳΔɺෳ਺ը໘(΍εϨου)Ͱಉ͡ঢ়ଶڞ༗͕ඞཁ • ࠶ར༻͕௿͍ • Ϗϡʔʹը໘ݻ༗ͷϩδοΫ( ViewStream )Λ࣋ͨͤΔͱɺଞͷը໘Ͱ࠶ར༻Ͱ͖ͳ͍ • ࣗ༝౓͕ߴ͘ɺ֤ࣗͷ࣮૷͕όϥόϥʹͳΓ͕ͪ 31
  11. 32.
  12. 33.

    Unio Unidirectional Input / Output framework with RxSwift. 5 class

    UnioStream<Logic: LogicType> { let input: Relay<Logic.Input> let output: Relay<Logic.Output> init(input: Logic.Input, state: Logic.State, extra: Logic.Extra, logic: Logic) } 5 https://github.com/cats-oss/Unio 33
  13. 35.

    Unio.Input ೖྗͱͳΔετϦʔϜͷू໿ɻUnioStreamͷґଘͱͳΔ extension GitHubSearchLogicStream { struct Input: InputType { let

    searchText = PublishRelay<String?>() } struct Output: OutputType { let repositories: BehaviorRelay<[GitHub.Repository]> let error: Observable<Error> } struct State: StateType { let repositories = BehaviorRelay<[GitHub.Repository]>(value: []) } struct Extra: ExtraType { let searchAPIStream: GitHubSearchAPIStreamType let scheduler: SchedulerType } } // View͔ΒΠϕϯτΛૹ৴͢Δ input.accept("query", for: \.searchText) input.subscribe // NG 35
  14. 36.

    Unio.Output ग़ྗͱͳΔετϦʔϜͷू໿ extension GitHubSearchLogicStream { struct Input: InputType { let

    searchText = PublishRelay<String?>() } struct Output: OutputType { let repositories: BehaviorRelay<[GitHub.Repository]> let error: Observable<Error> } struct State: StateType { let repositories = BehaviorRelay<[GitHub.Repository]>(value: []) } struct Extra: ExtraType { let searchAPIStream: GitHubSearchAPIStreamType let scheduler: SchedulerType } } // ViewͰ஋Λ؍ଌ͢Δ output.observable(for: \.repositories).subscribe(onNext: { print($0) }) output.accept // NG 36
  15. 37.

    Unio.State UnioStreamͷ಺෦ঢ়ଶ extension GitHubSearchLogicStream { struct Input: InputType { let

    searchText = PublishRelay<String?>() } struct Output: OutputType { let repositories: BehaviorRelay<[GitHub.Repository]> let error: Observable<Error> } struct State: StateType { let repositories = BehaviorRelay<[GitHub.Repository]>(value: []) } struct Extra: ExtraType { let searchAPIStream: GitHubSearchAPIStreamType let scheduler: SchedulerType } } 37
  16. 38.

    Unio.Extra InputҎ֎ͷUnioStreamͷґଘɺAPIΫϥΠΞϯτͳͲ extension GitHubSearchLogicStream { struct Input: InputType { let

    searchText = PublishRelay<String?>() } struct Output: OutputType { let repositories: BehaviorRelay<[GitHub.Repository]> let error: Observable<Error> } struct State: StateType { let repositories = BehaviorRelay<[GitHub.Repository]>(value: []) } struct Extra: ExtraType { let searchAPIStream: GitHubSearchAPIStreamType let scheduler: SchedulerType } } 38
  17. 39.

    Unio.Logic InputɺStateɺExtraΛ΋ͱʹOutputͷੜ੒΍ϩδοΫΛ࣮ߦ͢Δ extension GitHubSearchLogicStream.Logic { func bind(from dependency: Dependency<Input, State,

    Extra>) -> Output { let state = dependency.state let extra = dependency.extra let searchAPIStream = extra.searchAPIStream dependency.inputObservable(for: \.searchText) .debounce(0.3, scheduler: extra.scheduler) .flatMap { query -> Observable<String> in guard let query = query, !query.isEmpty else { return .empty() } return .just(query) } .bind(to: searchAPIStream.input.accept(for: \.searchRepository)) .disposed(by: disposeBag) searchAPIStream.output .observable(for: \.searchResponse) .map { $0.items } .bind(to: state.repositories) .disposed(by: disposeBag) return Output(repositories: state.repositories, error: searchAPIStream.output.observable(for: \.searchError)) } } 39
  18. 45.
  19. 47.

    Myvideo Flux • MyvideoAction func addMyvideo(_ myvideo: Myvideo) func removeMyvideo(with

    id: Myvideo.ID) • MyvideoStore let allMyvideo: Property<[Myvideo]> • Shared Flux (Provider) final class Flux { static let shared = Flux() private init() {} private(set) lazy var myvideoAction: MyvideoAction = .shared private(set) lazy var myvideoDispatcher: MyvideoDispatcher = .shared private(set) lazy var myvideoStore: MyvideoStore = .shared } 47
  20. 49.

    MyvideoLogicStream /// ϚΠϏσΦ΁ͷ௥Ճɾ࡟আΛ୲͏ extension MyvideoLogicStream { struct Input: InputType {

    let addMyvideo = PublishRelay<Myvideo>() let removeMyvideo = PublishRelay<Myvideo.ID>() } struct Output: OutputType {} typealias State = NoState struct Extra: ExtraType { let flux: Flux init(flux: Flux = .shared) { self.flux = flux } } // ... } 49
  21. 50.

    MyvideoLogicStream.Logic.bind(from:) extension MyvideoLogicStream.Logic { func bind(from dependency: Dependency<Input, State, Extra>)

    -> Output { let flux = dependency.extra.flux let myvideoAction = flux.myvideoAction let myvideoStore = flux.myvideoStore dependency.inputObservable(for: \.addMyvideo) .subscribe(onNext: myvideoAction.addMyvideo) .disposed(by: disposeBag) dependency.inputObservable(for: \.removeMyvideo) .subscribe(onNext: myvideoAction.removeMyvideo) .disposed(by: disposeBag) return Output() } } 50
  22. 51.

    EpisodeViewStream extension EpisodeViewStream { struct Input: InputType { let episodeID

    = PublishRelay<Episode.ID>() let didTapMyvideoButton = PublishRelay<Void>() } struct Output: OutputType { let episode: Observable<Episode> let showErrorView: Observable<Void> let isLoading: Observable<Bool> let isMyvideo: BehaviorRelay<Bool> } struct State: StateType { fileprivate let isMyvideo = BehaviorRelay<Bool>(value: false) } struct Extra: ExtraType { let myvideoLogicStream: MyvideoLogicStreamType let apiClient: APIClient let flux: Flux init(myvideoLogicStream: MyvideoLogicStreamType = MyvideoLogicStream(), apiClient: APIClient = .shared, flux: Flux = .shared) { self.myvideoLogicStream = myvideoLogicStream self.apiClient = apiClient self.flux = flux } } // ... } 51
  23. 52.

    EpisodeViewStream.Logic.bind(from:) dependency.inputObservable(for: \.episodeID) .subscribe(onNext: { id in fetchEpisodeAction.execute(id) }) .disposed(by:

    disposeBag) dependency.inputObservable(for: \.didTapMyvideoButton) .withLatestFrom(episode) { $1 } .map { Myvideo(from: $0) } .subscribe(onNext: { myvideo in if isMyvideo.value { myvideoLogicStream.input.accept(myvideo.id, for: \.removeMyvideo) } else { myvideoLogicStream.input.accept(myvideo, for: \.addMyvideo) } }) .disposed(by: disposeBag) myvideoStore.allMyvideo.asObservable() .withLatestFrom(episode) { ($0, $1) } .map { (myvideos, episode) in myvideos.contains(episode) } .bind(to: isMyvideo) .disposed(by: disposeBag) 52
  24. 53.

    EpisodeViewController override func viewDidLoad() { super.viewDidLoad() input: do { myvideoButton.rx.tap

    .bind(to: viewStream.input.accept(for: \.didTapMyvideoButton)) .disposed(by: disposeBag) } output: do { viewStream.output.observable(for: \.showErrorView) .map { false } .bind(to: errorView.rx.isHidden) .disposed(by: disposeBag) viewStream.output.observable(for: \.isLoading) .map { !$0 } .bind(to: loadingView.rx.isHidden) .disposed(by: disposeBag) viewStream.output.observable(for: \.isMyvideo) .map { $0 ? "add myvideo" : "remove myvideo" } .subscribe(onNext: { [weak self] text in guard let me = self else { return } me.myvideoButton.titleLabel?.text = text }) .disposed(by: disposeBag) } } 53
  25. 55.

    MyvideoListViewStream extension MyvideoListViewStream { /// Binding Model by `RxDataSources` typealias

    SectionModel = AnimatableSectionModel<Section, Element> struct Input: InputType {} struct Output: OutputType { let sectionModels: Observable<[SectionModel]> let isShowEmptyView: Observable<Bool> } struct State: StateType { fileprivate let allMyvideo = BehaviorRelay<[Myvideo]>(value: []) } struct Extra: ExtraType { let flux: Flux init(flux: Flux = .shared) { self.flux = flux } } // ... } 55
  26. 56.

    MyvideoListViewStream.Logic.bind(from:) func bind(from dependency: Dependency<Input, State, Extra>) -> Output {

    let state = dependency.state let allMyvideo = state.allMyvideo let flux = dependency.extra.flux let myvideoStore = flux.myvideoStore myvideoStore.allMyvideo.asObservable() .bind(to: allMyvideo) .disposed(by: disposeBag) let sectionModels = allMyvideo .map { items -> [SectionModel] in if items.isEmpty { return [] } else { return [SectionModel(model: .myvideo, items: items)] } } .share() let isShowEmptyView = sectionModels.map { $0.isEmpty } return Output(sectionModels: sectionModels, isShowEmptyView: isShowEmptyView) } 56
  27. 57.

    MyvideoListViewController override func viewDidLoad() { super.viewDidLoad() output: do { viewStream.output.observable(for:

    \.sectionModels) .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) viewStream.output.observable(for: \.isShowEmptyView) .bind(to: emptyView.rx.isHidden) .disposed(by: disposeBag) } } 57
  28. 58.

    Conclusion • ΞϓϦͷઃܭ͸ɺͲͷΑ͏ʹෳࡶͳঢ়ଶΛ೺Ѳɾ؅ཧ͢Δ͔͕伴 • MVVM + Flux ΛదࡐదॴͰ࢖͍෼͚͍ͯΔ • ը໘Λڞ༗͍ͨ͠σʔλΛѻ͏:

    Flux + MVVM • Ұը໘ʹऩ·Δ / ֊૚ؔ܎ͷϞδϡʔϧʹด͡ΔσʔλΛѻ͏: MVVM • ࣗ༝ͳ࣮૷ʹͳΓ͕ͪͳViewModelʹடংΛ༩͑ΔͨΊUnioΛ࢖༻ ͍ͯ͠Δ 58
  29. 59.