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

iOS Test Night #3: ViewController ⇄ State by RxSwift

iOS Test Night #3: ViewController ⇄ State by RxSwift

Takeshi Ihara

March 13, 2017
Tweet

More Decks by Takeshi Ihara

Other Decks in Programming

Transcript

  1. ViewController ⁶ Presenter
    by RxSwift
    iOS Test Night #3

    View Slide

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

    View Slide

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

    View Slide

  4. ViewController ⁶ Presenter

    View Slide

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

    View Slide

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

    View Slide

  7. ViewController (VC)
    Presenter
    State Event

    View Slide

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

    View Slide

  9. 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)
    }
    }

    View Slide

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


    }

    View Slide

  11. class Presenter {

    let eventReciever = PublishSubject()
    private let innerViewRefrector =
    BehaviorSubject(value: .initial)
    private let disposeBag = DisposeBag()
    var viewRefrector: Observable {
    return innerViewRefrector.asObservable()
    }

    }
    ௨஌ɾड৴ܥͷαϒδΣΫτ

    View Slide

  12. 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)
    }
    }

    View Slide

  13. 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͔Βड͚औΔΠϕϯτ
    ௨஌ɾड৴ܥͷαϒδΣΫτ
    ΠϕϯτΛड͚औͬͯ
    ঢ়ଶΛฦ͢

    View Slide

  14. 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)
    }
    }

    View Slide

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

    }

    View Slide

  16. 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)
    }

    }

    View Slide

  17. 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)
    }
    }

    View Slide

  18. 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ΛૹΔ

    View Slide

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

    View Slide

  20. ViewController (VC)
    Presenter
    State Event

    View Slide

  21. Test

    View Slide

  22. ViewController (VC)
    Presenter
    State Event

    View Slide

  23. ViewController (VC)
    Presenter
    State Event
    Testର৅

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  29. // 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)
    }

    View Slide

  30. Test Case

    View Slide

  31. 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)))
    }
    }
    }
    }
    }

    View Slide

  32. 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()
    }

    }
    }
    }

    View Slide

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

    }
    }
    }
    }
    }

    View Slide

  34. 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)))
    }
    }
    }
    }
    }
    ड͚ͱͬͨঢ়ଶͷ
    ൃੜͨ࣌͠ࠁɾछྨΛݕূ

    View Slide

  35. 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)))
    }
    }
    }
    }
    }
    ड͚ͱͬͨঢ়ଶͷ
    ൃੜͨ࣌͠ࠁɾछྨΛݕূ
    Ծ૝࣌ࠁ্Ͱͷ
    ΠϕϯτϋϯυϦϯάΛઃఆ

    View Slide

  36. ViewController (VC)
    Presenter
    State Event

    View Slide

  37. ·ͱΊ
    • ViewͷඳըͳͲʹґଘ͠ͳ͍γϯϓϧͳ

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

    View Slide