Abema iOS Architecture

3925ee6eaa41031bac799de0f4f528ec?s=47 to4iki
April 25, 2019

Abema iOS Architecture

3925ee6eaa41031bac799de0f4f528ec?s=128

to4iki

April 25, 2019
Tweet

Transcript

  1. AbemaTV iOS Archetecture 2019.04.25 ಥܸ!!ྡͷΞʔΩςΫνϟ @to4iki 1

  2. About Me • Takezawa Toshiki • @to4iki • iOS/Swift 

    2
  3. Agenda • ΞϓϦ։ൃʹ͓͚Δઃܭ • AbemaTVͰͷ࣮૷ 3

  4. ΞϓϦ։ൃ ʹ͓͚Δઃܭ 4

  5. ΫϥΠΞϯταΠυϓϩάϥϛϯάͷओ໨త • APIϨεϙϯεΛͲͷΑ͏ʹը໘ʹ൓ө͢Δ͔ • ෳࡶͳมԽ͠͏Δσʔλঢ়ଶΛͲͷΑ͏ʹը໘ʹ൓ө͢Δ͔ • ཧ૝తͳUI/UXΛͲͷΑ͏ʹը໘ʹ൓ө͢Δ͔ 5

  6. ΫϥΠΞϯταΠυϓϩάϥϛϯάͷओ໨త͸ɺ ͲͷΑ͏ʹը໘ʹ൓ө͢Δ͔ 6

  7. Presentation Domain Separation(PDS)1 ͲͷΑ͏ʹը໘ʹ൓ө͢Δ͔ = Presentation ʹओ୊Λஔ͍ͯ࿩ΛਐΊ·͢ 1 https://martinfowler.com/bliki/PresentationDomainSeparation.html 7

  8. ΞϓϦ։ൃͷ՝୊ 8

  9. ը໘ʹ൓ө͢Δ ঢ়ଶ͕ෳࡶ 9

  10. AbemaTVͷঢ়ଶ 10

  11. ෳࡶͩͱԿ͕ਏ͍ͷ͔ ঢ়ଶ͕ • ͍ͭ • ͲͷΑ͏ʹ • Ͳ͏ͯ͠ ߋ৽͞ΕΔͷ͔Λ೺Ѳ͢Δͷ͕ਏ͍ 11

  12. ΞϓϦͷઃܭ ͲͷΑ͏ʹෳࡶͳঢ়ଶΛ ೺Ѳɺ؅ཧ͢Δ͔͕伴 12

  13. ͲͷΑ͏ʹղܾ͠Α͏ͱ ͍ͯ͠Δ͔ 13

  14. MVVM + Flux 14

  15. ͓͞Β͍͔Β 15

  16. Flux 16

  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
  18. Dispatcher Action͔ΒྲྀΕ͖ͯͨΠϕϯτΛStoreʹྲྀ͢ 18

  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
  20. Action ViewͳͲ͔Βͷૢ࡞ʹΑΓΠϕϯτΛൃੜͤ͞Δ 20

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

    https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean- architecture.html 21
  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
  23. Store Dispatcher͔ΒྲྀΕ͖ͯͨΠϕϯτ಺ͷ஋Λอ࣋͢Δ 23

  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
  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
  26. Flux Pros/Cons Pros • Ұํ޲ͷσʔλϑϩʔʹΑΓϝοηʔδϯά΍ϧʔςΟϯάॲཧ͕໌֬ • ը໘Λލ͙৔߹͸ɺFluxతͳΞϓϩʔν͸Ͳ͏ͯ͠΋ඞཁʹͳΔ • ݁߹ςετָ͕ Cons

    • ෳ਺ը໘͔Β subscribe ͞ΕΔͨΊॲཧ͕௥͍ͮΒ͍ • Store͕େྔͷϩδοΫΛ࣋ͭΑ͏ʹͳΔ 26
  27. MVVM 27

  28. MVVM • Model: ϏδωεϩδοΫΛ࣋ͭ • View: Ϣʔβʔ͔ΒͷೖྗΛड͚औΔ • ViewModel: ϓϨθϯςʔγϣϯʹؔ͢ΔϩδοΫɾঢ়ଶΛ࣋ͭ

    28
  29. ViewModel • άϩʔόϧͰঢ়ଶอ࣋͢Δඞཁ͕ͳ͍৔໘Ͱ࢖༻ɺ (ݪଇ)FluxΛॖখͤ͞Δํ਑ • AbemaͰ͸ ViewStream ͱݺΜͰΔ • ViewController

    ͱ1:1ͷؔ܎ʹͳΔ͜ͱ͕ଟ͍ • ViewStream ͕֊૚ߏ଄ʹͳΔ͜ͱ΋ 29
  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
  31. MVVM Pros/Cons Pros • ঢ়ଶͷϥΠϑαΠΫϧ؅ཧָ͕ • ViewStream ͷ deinit Ͱࣗಈॲཧ

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

  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
  34. Unio • UnioStream಺ͰҰํ௨ߦͷσʔλϑϩʔΛ࣮ݱ͠ɺσʔλϑϩʔΛ௥͍΍͘͢͢Δ • ςϯϓϨʔτ࣮૷ʹΑΓ࣮૷Λڲਖ਼͢Δ 34

  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
  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
  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
  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
  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
  40. MVVM + Flux ͷ ࢖͍ॴ 40

  41. MVVM + Flux • ΞϓϦશମͰ࢖͏σʔλΛѻ͏ ར༻͸ඞཁ࠷௿ݶ 41

  42. MVVM • 1ը໘Ͱऩ·ΔσʔλΛѻ͏ 42

  43. e.g. MVVM + Flux ϚΠϏσΦػೳ ※ AbemaTVͰ࠾༻͍ͯ͠Δ࣮૷ʹ͍ۙܗࣜ 43

  44. ػೳ • Τϐιʔυը໘ • ίϯςϯπΛϚΠϏσΦొ࿥͢Δ • ίϯςϯπΛϚΠϏσΦղআ͢Δ • ϚΠϏσΦҰཡը໘ •

    ϚΠϏσΦొ࿥ͨ͠Ұཡ͕දࣔ͞ΕΔ • 0݅ͷ৔߹͸ۭදࣔͱͳΔ 44
  45. ߏ੒ 45

  46. Myvideo Flux 46

  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
  48. Episode + Myvideo 48

  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
  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
  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
  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
  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
  54. MyvideoList 54

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

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

  60. SeeAlso • Flux with RxSwift https://speakerdeck.com/dekatotoro/flux-with-rxswift • MVVM + Flux

    https://speakerdeck.com/martysuzuki/mvvm-plus-flux 60