Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

ΞϓϦ։ൃͷ՝୊ 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

AbemaTVͷঢ়ଶ 10

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

MVVM + Flux 14

Slide 15

Slide 15 text

͓͞Β͍͔Β 15

Slide 16

Slide 16 text

Flux 16

Slide 17

Slide 17 text

Flux Data in a Flux application flows in a single direction 2 2 https://facebook.github.io/flux/docs/in-depth-overview.html 17

Slide 18

Slide 18 text

Dispatcher Action͔ΒྲྀΕ͖ͯͨΠϕϯτΛStoreʹྲྀ͢ 18

Slide 19

Slide 19 text

Dispatcher ActionType ͷ୅ΘΓʹઐ༻ͷ DispatchSubject Λෳ਺༻ҙ ※ DispatchSubject: PublishSubject ͷϥούʔ final class Dispatcher { static let shared = Dispatcher() let someModel = DispatchSubject let isLoading = DispatchSubject let error = DispatchSubject } 19

Slide 20

Slide 20 text

Action ViewͳͲ͔Βͷૢ࡞ʹΑΓΠϕϯτΛൃੜͤ͞Δ 20

Slide 21

Slide 21 text

Action • Clean Architecture3 ͷ֎ԁɺ Devices, Web, DB, UIΛActionͱ͢Δ 3 https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean- architecture.html 21

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Store Dispatcher͔ΒྲྀΕ͖ͯͨΠϕϯτ಺ͷ஋Λอ࣋͢Δ 23

Slide 24

Slide 24 text

Store • ঢ়ଶ: Property • ίϚϯυ: Observable class Store { /// state let isLoading: Property /// command let showErrorView: Observable init(dispatcher: Dispatcher = .shared) { // dispatcher͔ΒྲྀΕ͖ͯͨΠϕϯτΛbind͢Δ } } 24

Slide 25

Slide 25 text

Rx.Peoperty? A get-only BehaviorRelay that is (almost) equivalent to ReactiveSwift's Property. 4 set ΍ bind ͕ग़དྷͳ͍ BehaviorRelay class Store { let value: Property private let _value = BehaviorRelay(value: 0) init() { self.value = Property(_value) } } store.value.accept(1) // error 4 https://github.com/inamiy/RxProperty 25

Slide 26

Slide 26 text

Flux Pros/Cons Pros • Ұํ޲ͷσʔλϑϩʔʹΑΓϝοηʔδϯά΍ϧʔςΟϯάॲཧ͕໌֬ • ը໘Λލ͙৔߹͸ɺFluxతͳΞϓϩʔν͸Ͳ͏ͯ͠΋ඞཁʹͳΔ • ݁߹ςετָ͕ Cons • ෳ਺ը໘͔Β subscribe ͞ΕΔͨΊॲཧ͕௥͍ͮΒ͍ • Store͕େྔͷϩδοΫΛ࣋ͭΑ͏ʹͳΔ 26

Slide 27

Slide 27 text

MVVM 27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

ViewModel • άϩʔόϧͰঢ়ଶอ࣋͢Δඞཁ͕ͳ͍৔໘Ͱ࢖༻ɺ (ݪଇ)FluxΛॖখͤ͞Δํ਑ • AbemaͰ͸ ViewStream ͱݺΜͰΔ • ViewController ͱ1:1ͷؔ܎ʹͳΔ͜ͱ͕ଟ͍ • ViewStream ͕֊૚ߏ଄ʹͳΔ͜ͱ΋ 29

Slide 30

Slide 30 text

ViewStream InputͷΠϕϯτετϦʔϜΛݩʹOutputͷετϦʔϜΛੜ੒͢Δ final class ViewStream { // output let state: Property let event: Observable // input init(viewDidAppear: Observable, didTapButton: Observable) { // inputͷΠϕϯτετϦʔϜΛ΋ͱʹoutputΛੜ੒͢Δ } } 30

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Unio 32

Slide 33

Slide 33 text

Unio Unidirectional Input / Output framework with RxSwift. 5 class UnioStream { let input: Relay let output: Relay init(input: Logic.Input, state: Logic.State, extra: Logic.Extra, logic: Logic) } 5 https://github.com/cats-oss/Unio 33

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Unio.Input ೖྗͱͳΔετϦʔϜͷू໿ɻUnioStreamͷґଘͱͳΔ extension GitHubSearchLogicStream { struct Input: InputType { let searchText = PublishRelay() } struct Output: OutputType { let repositories: BehaviorRelay<[GitHub.Repository]> let error: Observable } 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

Slide 36

Slide 36 text

Unio.Output ग़ྗͱͳΔετϦʔϜͷू໿ extension GitHubSearchLogicStream { struct Input: InputType { let searchText = PublishRelay() } struct Output: OutputType { let repositories: BehaviorRelay<[GitHub.Repository]> let error: Observable } 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

Slide 37

Slide 37 text

Unio.State UnioStreamͷ಺෦ঢ়ଶ extension GitHubSearchLogicStream { struct Input: InputType { let searchText = PublishRelay() } struct Output: OutputType { let repositories: BehaviorRelay<[GitHub.Repository]> let error: Observable } struct State: StateType { let repositories = BehaviorRelay<[GitHub.Repository]>(value: []) } struct Extra: ExtraType { let searchAPIStream: GitHubSearchAPIStreamType let scheduler: SchedulerType } } 37

Slide 38

Slide 38 text

Unio.Extra InputҎ֎ͷUnioStreamͷґଘɺAPIΫϥΠΞϯτͳͲ extension GitHubSearchLogicStream { struct Input: InputType { let searchText = PublishRelay() } struct Output: OutputType { let repositories: BehaviorRelay<[GitHub.Repository]> let error: Observable } struct State: StateType { let repositories = BehaviorRelay<[GitHub.Repository]>(value: []) } struct Extra: ExtraType { let searchAPIStream: GitHubSearchAPIStreamType let scheduler: SchedulerType } } 38

Slide 39

Slide 39 text

Unio.Logic InputɺStateɺExtraΛ΋ͱʹOutputͷੜ੒΍ϩδοΫΛ࣮ߦ͢Δ extension GitHubSearchLogicStream.Logic { func bind(from dependency: Dependency) -> 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 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

Slide 40

Slide 40 text

MVVM + Flux ͷ ࢖͍ॴ 40

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

ػೳ • Τϐιʔυը໘ • ίϯςϯπΛϚΠϏσΦొ࿥͢Δ • ίϯςϯπΛϚΠϏσΦղআ͢Δ • ϚΠϏσΦҰཡը໘ • ϚΠϏσΦొ࿥ͨ͠Ұཡ͕දࣔ͞ΕΔ • 0݅ͷ৔߹͸ۭදࣔͱͳΔ 44

Slide 45

Slide 45 text

ߏ੒ 45

Slide 46

Slide 46 text

Myvideo Flux 46

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Episode + Myvideo 48

Slide 49

Slide 49 text

MyvideoLogicStream /// ϚΠϏσΦ΁ͷ௥Ճɾ࡟আΛ୲͏ extension MyvideoLogicStream { struct Input: InputType { let addMyvideo = PublishRelay() let removeMyvideo = PublishRelay() } struct Output: OutputType {} typealias State = NoState struct Extra: ExtraType { let flux: Flux init(flux: Flux = .shared) { self.flux = flux } } // ... } 49

Slide 50

Slide 50 text

MyvideoLogicStream.Logic.bind(from:) extension MyvideoLogicStream.Logic { func bind(from dependency: Dependency) -> 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

Slide 51

Slide 51 text

EpisodeViewStream extension EpisodeViewStream { struct Input: InputType { let episodeID = PublishRelay() let didTapMyvideoButton = PublishRelay() } struct Output: OutputType { let episode: Observable let showErrorView: Observable let isLoading: Observable let isMyvideo: BehaviorRelay } struct State: StateType { fileprivate let isMyvideo = BehaviorRelay(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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

MyvideoList 54

Slide 55

Slide 55 text

MyvideoListViewStream extension MyvideoListViewStream { /// Binding Model by `RxDataSources` typealias SectionModel = AnimatableSectionModel struct Input: InputType {} struct Output: OutputType { let sectionModels: Observable<[SectionModel]> let isShowEmptyView: Observable } struct State: StateType { fileprivate let allMyvideo = BehaviorRelay<[Myvideo]>(value: []) } struct Extra: ExtraType { let flux: Flux init(flux: Flux = .shared) { self.flux = flux } } // ... } 55

Slide 56

Slide 56 text

MyvideoListViewStream.Logic.bind(from:) func bind(from dependency: Dependency) -> 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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Conclusion • ΞϓϦͷઃܭ͸ɺͲͷΑ͏ʹෳࡶͳঢ়ଶΛ೺Ѳɾ؅ཧ͢Δ͔͕伴 • MVVM + Flux ΛదࡐదॴͰ࢖͍෼͚͍ͯΔ • ը໘Λڞ༗͍ͨ͠σʔλΛѻ͏: Flux + MVVM • Ұը໘ʹऩ·Δ / ֊૚ؔ܎ͷϞδϡʔϧʹด͡ΔσʔλΛѻ͏: MVVM • ࣗ༝ͳ࣮૷ʹͳΓ͕ͪͳViewModelʹடংΛ༩͑ΔͨΊUnioΛ࢖༻ ͍ͯ͠Δ 58

Slide 59

Slide 59 text

Thanks 59

Slide 60

Slide 60 text

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