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 full-size slide

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

    View full-size slide

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

    View full-size slide

  4. ViewController ⁶ Presenter

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. ViewController (VC)
    Presenter
    State Event

    View full-size slide

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

    View full-size 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 full-size slide

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


    }

    View full-size slide

  11. class Presenter {

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

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

    View full-size 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 full-size 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 full-size 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 full-size slide

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

    }

    View full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

  20. ViewController (VC)
    Presenter
    State Event

    View full-size slide

  21. ViewController (VC)
    Presenter
    State Event

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. // 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 full-size slide

  29. 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 full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

  34. ViewController (VC)
    Presenter
    State Event

    View full-size slide

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

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

    View full-size slide