Slide 1

Slide 1 text

ViewController ⁶ Presenter by RxSwift iOS Test Night #3

Slide 2

Slide 2 text

ࣗݾ঺հ • Takeshi Ihara / @nonchalant0303 • Recruit Marketing Partners • iOS Engineer

Slide 3

Slide 3 text

RxSwift Rx is a generic abstraction of computation expressed through Observable interface. This is a Swift version of Rx.

Slide 4

Slide 4 text

ViewController ⁶ Presenter

Slide 5

Slide 5 text

ViewController • ը໘දࣔ΍ϢʔβͷλονΠϕϯτͳͲͷ EventΛPresenterʹ௨஌͢Δ • Presenter͔Βड͚औͬͨStateʹΑΓView ͷදࣔΛ੾Γସ͑Δ

Slide 6

Slide 6 text

Presenter • View͔ΒEventΛड͚औΓɺඞཁ͕͋Ε͹ EventʹԠͨ͡UseCaseΛ࣮ߦ͢Δ • UseCase͔Βड͚औͬͨσʔλΛStateͱ͠ ͯView΁౉͢

Slide 7

Slide 7 text

ViewController (VC) Presenter State Event

Slide 8

Slide 8 text

prepare VC viewWillAppear Presenter → Event State ← prepared "1*$BMM Refresh View touch touched %POPUIJOH Refresh View touchEvent

Slide 9

Slide 9 text

class Presenter { enum State { case initial case prepared(value: Int) case touched } enum Event { case prepare case touch } let eventReciever = PublishSubject() private let innerViewRefrector = BehaviorSubject(value: .initial) private let disposeBag = DisposeBag() var viewRefrector: Observable { return innerViewRefrector.asObservable() } init() { eventReciever .flatMap { event -> Observable in switch event { case .prepare: return [API Call] .map { .prepared(value: $0) } case .touch: return Observable.just(.touched) } } .bindTo(innerViewRefrector) .addDisposableTo(disposeBag) } }

Slide 10

Slide 10 text

class Presenter { enum State { // VCʹฦ͢ঢ়ଶ case initial case prepared(value: Int) case touched } enum Event { // VC͔Βड͚औΔΠϕϯτ case prepare case touch } 
 … }

Slide 11

Slide 11 text

class Presenter { … let eventReciever = PublishSubject() private let innerViewRefrector = BehaviorSubject(value: .initial) private let disposeBag = DisposeBag() var viewRefrector: Observable { return innerViewRefrector.asObservable() } … } ௨஌ɾड৴ܥͷαϒδΣΫτ

Slide 12

Slide 12 text

class Presenter { … init() { eventReciever .flatMap { event -> Observable in switch event { case .prepare: return [API Call] .map { .prepared(value: $0) } case .touch: return Observable.just(.touched) } } .bindTo(innerViewRefrector) .addDisposableTo(disposeBag) } }

Slide 13

Slide 13 text

class Presenter { enum State { case initial case prepared(value: Int) case touched } enum Event { case prepare case touch } let eventReciever = PublishSubject() private let innerViewRefrector = BehaviorSubject(value: .initial) private let disposeBag = DisposeBag() var viewRefrector: Observable { return innerViewRefrector.asObservable() } init() { eventReciever .flatMap { event -> Observable in switch event { case .prepare: return [API Call] .map { .prepared(value: $0) } case .touch: return Observable.just(.touched) } } .bindTo(innerViewRefrector) .addDisposableTo(disposeBag) } } VCʹฦ͢ঢ়ଶ VC͔Βड͚औΔΠϕϯτ ௨஌ɾड৴ܥͷαϒδΣΫτ ΠϕϯτΛड͚औͬͯ ঢ়ଶΛฦ͢

Slide 14

Slide 14 text

class ViewController: UIViewController { typealias Event = Presenter.Event var presenter: Presenter! private let disposeBag = DisposeBag() override func viewDidLoad() { setupUIBindings() setupEventBindings() } private func setupUIBindings() { presenter.viewRefrector .asDriver(onErrorDriveWith: .empty()) .drive( onNext: { state in switch state { case .initial: break case .prepared(let value): // API͔Βͷ݁ՌΛ༻͍ͯViewʹ൓ө͢Δ case .touched: // Touchͷ݁ՌΛViewʹ൓ө͢Δ } } ) .addDisposableTo(disposeBag) } private func setupEventBindings() { rx.sentMessage(#selector(UIViewController.viewWillAppear(_:))) .map { _ in () } .shareReplay(1) .map { return Event.prepare } .bindTo(presenter.eventReciever) .addDisposableTo(disposeBag) btn.rx.tap .map { return Event.touch } .bindTo(presenter.eventReciever) .addDisposableTo(disposeBag) } }

Slide 15

Slide 15 text

class ViewController: UIViewController { typealias Event = Presenter.Event var presenter: Presenter! private let disposeBag = DisposeBag() override func viewDidLoad() { setupUIBindings() setupEventBindings() } … }

Slide 16

Slide 16 text

class ViewController: UIViewController { … private func setupUIBindings() { presenter.viewRefrector .asDriver(onErrorDriveWith: .empty()) .drive( onNext: { state in switch state { case .initial: break case .prepared(let value): // API͔Βͷ݁ՌΛ༻͍ͯViewʹ൓ө͢Δ case .touched: // Touchͷ݁ՌΛViewʹ൓ө͢Δ } } ) .addDisposableTo(disposeBag) } … }

Slide 17

Slide 17 text

class ViewController: UIViewController { … private func setupEventBindings() { rx.sentMessage(#selector(UIViewController.viewWillAppear(_:))) // viewWillAppearΛPresenterʹྲྀͯ͠Δ .map { _ in () } .shareReplay(1) .map { return Event.prepare } .bindTo(presenter.eventReciever) .addDisposableTo(disposeBag) btn.rx.tap // ϘλϯͷλονΠϕϯτΛPresenterʹྲྀͯ͠Δ .map { return Event.touch } .bindTo(presenter.eventReciever) .addDisposableTo(disposeBag) } }

Slide 18

Slide 18 text

class ViewController: UIViewController { typealias Event = Presenter.Event var presenter: Presenter! private let disposeBag = DisposeBag() override func viewDidLoad() { setupUIBindings() setupEventBindings() } private func setupUIBindings() { presenter.viewRefrector .asDriver(onErrorDriveWith: .empty()) .drive( onNext: { state in switch state { case .initial: break case .prepared(let value): // API͔Βͷ݁ՌΛ༻͍ͯViewʹ൓ө͢Δ case .touched: // Touchͷ݁ՌΛViewʹ൓ө͢Δ } } ) .addDisposableTo(disposeBag) } private func setupEventBindings() { rx.sentMessage(#selector(UIViewController.viewWillAppear(_:))) .map { _ in () } .shareReplay(1) .map { return Event.prepare } .bindTo(presenter.eventReciever) .addDisposableTo(disposeBag) btn.rx.tap .map { return Event.touch } .bindTo(presenter.eventReciever) .addDisposableTo(disposeBag) } } StateΛड͚औͬͯ ViewΛ൓ө͢Δ EventΛૹΔ

Slide 19

Slide 19 text

prepare VC viewWillAppear Presenter → Event State ← prepared "1*$BMM Refresh View touch touched %POPUIJOH Refresh View touchEvent

Slide 20

Slide 20 text

ViewController (VC) Presenter State Event

Slide 21

Slide 21 text

Test

Slide 22

Slide 22 text

ViewController (VC) Presenter State Event

Slide 23

Slide 23 text

ViewController (VC) Presenter State Event Testର৅

Slide 24

Slide 24 text

Test Case • Input: VC͔Βड͚औΔEvent • Output: Presenter͕VCʹૹΔState

Slide 25

Slide 25 text

prepare VC viewWillAppear Presenter → Event State ← prepared "1*$BMM Refresh View touch touched %POPUIJOH Refresh View touchEvent

Slide 26

Slide 26 text

%POPUIJOH "1*$BMM VC viewWillAppear Presenter → Event State ← prepare prepared Refresh View touch touched Refresh View touchEvent Test Case Test Case

Slide 27

Slide 27 text

Test with Rx • Rx͸σʔλ͕࣍ݩͱ͍͏୯ҐΛ͍࣋ͬͯΔ ͷͰɺ࣌ࠁʹΑͬͯҧ͏ঢ়ଶΛ͍࣋ͬͯΔ • ࣌ࠁʹґଘ͢Δςετ͸؀ڥґଘ͕େ͖͘ ෆ҆ఆͳςετʹͳΓ΍͍͢

Slide 28

Slide 28 text

RxTest • RxSwiftͷςετ༻ϑϨʔϜϫʔΫ • ࣮࣌ؒͱ͸ҟͳΔԾ૝࣌ؒʹج͖ͮΠϕϯ τΛൃੜͤ͞Δ࢓૊Έ

Slide 29

Slide 29 text

// 5FTUBCMF0CTFSWBCMF1SFTFOUFS4UBUFΛੜ੒ let observer = scheduler.createObserver(Presenter.State.self) let xs = scheduler.createColdObservable([ // Ծ૝࣌ࠁ100ޙʹprepareΠϕϯτΛૹΔ next(100, Presenter.UserEvent.prepare) ]) // Ծ૝࣌ࠁ100ʹxsΛeventReceiverʹbinding͢Δ // ஗ԆධՁ͕૸Δ scheduler.scheduleAt(100) { xs .bindTo(presenter.eventReceiver) .addDisposableTo(disposeBag) } // WJFX3FGMFDUFSΛobserverʹSubscribeͤ͞Δ scheduler.scheduleAt(200) { presenter.viewReflecter .subscribe(observer) .addDisposableTo(disposeBag) }

Slide 30

Slide 30 text

Test Case

Slide 31

Slide 31 text

class PresenterSpec: QuickSpec { override func spec() { describe("Presenter") { var presenter: Presenter! var scheduler: TestScheduler! var disposeBag: DisposeBag! beforeEach { scheduler = TestScheduler(initialClock: 0) presenter = Presenter() disposeBag = DisposeBag() } context("when prepare") { it("prepared") { let observer = scheduler.createObserver(Presenter.State.self) let xs = scheduler.createColdObservable([ next(100, Presenter.UserEvent.prepare) ]) scheduler.scheduleAt(100) { xs .bindTo(presenter.eventReceiver) .addDisposableTo(disposeBag) } scheduler.scheduleAt(200) { presenter.viewReflecter .subscribe(observer) .addDisposableTo(disposeBag) } scheduler.start() expect(observer.events.count).to(equal(2)) expect(observer.events[0].time).to(equal(200)) expect(observer.events[1].time).to(equal(300)) let initial = observer.events[0].value.element expect(initial).toNot(beNil()) expect(initial!).to(equal(Presenter.State.initial)) let subject = observer.events[1].value.element expect(subject).toNot(beNil()) expect(subject!).to(equal(Presenter.State.prepared(value: 1))) } } } } }

Slide 32

Slide 32 text

class PresenterSpec: QuickSpec { override func spec() { describe("Presenter") { var presenter: Presenter! var scheduler: TestScheduler! var disposeBag: DisposeBag! beforeEach { scheduler = TestScheduler(initialClock: 0) presenter = Presenter() disposeBag = DisposeBag() } … } } }

Slide 33

Slide 33 text

class PresenterSpec: QuickSpec { override func spec() { describe("Presenter") { … context("when prepare") { it("prepared") { let observer = scheduler.createObserver(Presenter.State.self) let xs = scheduler.createColdObservable([ next(100, Presenter.UserEvent.prepare) ]) scheduler.scheduleAt(100) { xs .bindTo(presenter.eventReceiver) .addDisposableTo(disposeBag) } scheduler.scheduleAt(200) { presenter.viewReflecter .subscribe(observer) .addDisposableTo(disposeBag) } scheduler.start() … } } } } }

Slide 34

Slide 34 text

class PresenterSpec: QuickSpec { override func spec() { describe("Presenter") { … context("when prepare") { it("prepared") { … expect(observer.events.count).to(equal(2)) expect(observer.events[0].time).to(equal(200)) expect(observer.events[1].time).to(equal(200)) let initial = observer.events[0].value.element expect(initial).toNot(beNil()) expect(initial!).to(equal(Presenter.State.initial)) let subject = observer.events[1].value.element expect(subject).toNot(beNil()) expect(subject!).to(equal(Presenter.State.prepared(value: 1))) } } } } } ड͚ͱͬͨঢ়ଶͷ ൃੜͨ࣌͠ࠁɾछྨΛݕূ

Slide 35

Slide 35 text

class PresenterSpec: QuickSpec { override func spec() { describe("Presenter") { var presenter: Presenter! var scheduler: TestScheduler! var disposeBag: DisposeBag! beforeEach { scheduler = TestScheduler(initialClock: 0) presenter = Presenter() disposeBag = DisposeBag() } context("when prepare") { it("prepared") { let observer = scheduler.createObserver(Presenter.State.self) let xs = scheduler.createColdObservable([ next(100, Presenter.UserEvent.prepare) ]) scheduler.scheduleAt(100) { xs .bindTo(presenter.eventReceiver) .addDisposableTo(disposeBag) } scheduler.scheduleAt(200) { presenter.viewReflecter .subscribe(observer) .addDisposableTo(disposeBag) } scheduler.start() expect(observer.events.count).to(equal(2)) expect(observer.events[0].time).to(equal(200)) expect(observer.events[1].time).to(equal(200)) let initial = observer.events[0].value.element expect(initial).toNot(beNil()) expect(initial!).to(equal(Presenter.State.initial)) let subject = observer.events[1].value.element expect(subject).toNot(beNil()) expect(subject!).to(equal(Presenter.State.prepared(value: 1))) } } } } } ड͚ͱͬͨঢ়ଶͷ ൃੜͨ࣌͠ࠁɾछྨΛݕূ Ծ૝࣌ࠁ্Ͱͷ ΠϕϯτϋϯυϦϯάΛઃఆ

Slide 36

Slide 36 text

ViewController (VC) Presenter State Event

Slide 37

Slide 37 text

·ͱΊ • ViewͷඳըͳͲʹґଘ͠ͳ͍γϯϓϧͳ
 ΠϕϯτϕʔεͷςετΛॻ͚ͨ • ·ͨɺෳ਺ͷΠϕϯτΛϋϯυϦϯά͢Δ ͜ͱͰෳࡶͳςετΛλΠϛϯάʹґଘ͞ ͤΔ͜ͱͳ͘ॻ͘͜ͱ͕ग़དྷΔ