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

Abema iOS Architecture

Avatar for to4iki to4iki
April 25, 2019

Abema iOS Architecture

Avatar for to4iki

to4iki

April 25, 2019
Tweet

More Decks by to4iki

Other Decks in Programming

Transcript

  1. Flux Data in a Flux application flows in a single

    direction 2 2 https://facebook.github.io/flux/docs/in-depth-overview.html 17
  2. 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
  3. Action • Clean Architecture3 ͷ֎ԁɺ Devices, Web, DB, UIΛActionͱ͢Δ 3

    https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean- architecture.html 21
  4. 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
  5. Store • ঢ়ଶ: Property<State> • ίϚϯυ: Observable<Event> class Store {

    /// state let isLoading: Property<Bool> /// command let showErrorView: Observable<Void> init(dispatcher: Dispatcher = .shared) { // dispatcher͔ΒྲྀΕ͖ͯͨΠϕϯτΛbind͢Δ } } 24
  6. 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
  7. 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
  8. MVVM Pros/Cons Pros • ঢ়ଶͷϥΠϑαΠΫϧ؅ཧָ͕ • ViewStream ͷ deinit Ͱࣗಈॲཧ

    • ୯ମςετָ͕ Cons • ϝοηʔδϯά͕ϦϨʔͩΒ͚ʹͳΔɺෳ਺ը໘(΍εϨου)Ͱಉ͡ঢ়ଶڞ༗͕ඞཁ • ࠶ར༻͕௿͍ • Ϗϡʔʹը໘ݻ༗ͷϩδοΫ( ViewStream )Λ࣋ͨͤΔͱɺଞͷը໘Ͱ࠶ར༻Ͱ͖ͳ͍ • ࣗ༝౓͕ߴ͘ɺ֤ࣗͷ࣮૷͕όϥόϥʹͳΓ͕ͪ 31
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. Conclusion • ΞϓϦͷઃܭ͸ɺͲͷΑ͏ʹෳࡶͳঢ়ଶΛ೺Ѳɾ؅ཧ͢Δ͔͕伴 • MVVM + Flux ΛదࡐదॴͰ࢖͍෼͚͍ͯΔ • ը໘Λڞ༗͍ͨ͠σʔλΛѻ͏:

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