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

Abema iOS Architecture

to4iki
April 25, 2019

Abema iOS Architecture

to4iki

April 25, 2019
Tweet

More Decks by to4iki

Other Decks in Programming

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. ΞϓϦ։ൃͷ՝୊
    8

    View full-size slide

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

    View full-size slide

  10. AbemaTVͷঢ়ଶ
    10

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  14. MVVM + Flux
    14

    View full-size slide

  15. ͓͞Β͍͔Β
    15

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  21. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  24. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  30. 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

    View full-size slide

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

    View full-size slide

  32. 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

    View full-size slide

  33. 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

    View full-size slide

  34. 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

    View full-size slide

  35. 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

    View full-size slide

  36. 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

    View full-size slide

  37. MVVM + Flux ͷ
    ࢖͍ॴ
    40

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  42. Myvideo Flux
    46

    View full-size slide

  43. 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

    View full-size slide

  44. Episode + Myvideo
    48

    View full-size slide

  45. 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

    View full-size slide

  46. 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

    View full-size slide

  47. 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

    View full-size slide

  48. 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

    View full-size slide

  49. 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

    View full-size slide

  50. MyvideoList
    54

    View full-size slide

  51. 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

    View full-size slide

  52. 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

    View full-size slide

  53. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide