Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
iOS Test Night #3: ViewController ⇄ State by Rx...
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Takeshi Ihara
March 13, 2017
Programming
1.1k
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
iOS Test Night #3: ViewController ⇄ State by RxSwift
https://testnight.connpass.com/event/49561/
Takeshi Ihara
March 13, 2017
More Decks by Takeshi Ihara
See All by Takeshi Ihara
iOSDC20200921: Feature Flagを適切に分類することでA/Bテストの運用コストを下げる
nonchalant
3
1.4k
iOSDC 20190906: 動画アプリの投げ銭機能における 消耗型課金の仕組みと実装
nonchalant
3
6.3k
iOSDC 20190906: 動画アプリの投げ銭機能における 消耗型課金の仕組みと実装 with 発表ノート
nonchalant
2
630
Sign In with Apple
nonchalant
1
2.4k
iOSDC RejectCon 20180915: Factoryの自動生成によりテストを書きやすくする
nonchalant
1
740
iOSDC 20180902: 小さくはじめる端末管理
nonchalant
2
1k
devsap 20180728: コード生成のススメ
nonchalant
0
140
potatotips #50: iOSは自動生成の夢を見るか?
nonchalant
0
2k
try! Swift Tokyo 2018: Best Docker Container in Swift
nonchalant
1
1.4k
Other Decks in Programming
See All in Programming
OSもどきOS
arkw
0
460
AI 時代のソフトウェア設計の学び方
masuda220
PRO
29
12k
Spring Security 実践 ─ GraphQL APIで実務に役立つ 認証・認可 を学ぶ
wagyu
0
170
AutonomyとControlのあいだ:Graflowで記述するAIエージェント協調
myui
0
110
さぁV100、メモリをお食べ・・・
nilpe
0
130
[2026年度第1回ORセミナー] 計画最適化ベンチャーと競技プログラミング人材
terryu16
0
250
軽量Java基盤の設計 DIコンテナに頼らない、長期保守と1秒起動の実現 JJUG CCC 2026 Spring
macha64
0
460
AIチームを指揮するOSS「TAKT」活用術 / How to Use “TAKT,” an OSS Tool for Orchestrating AI Teams
nrslib
6
840
正しくソフトウェアを作る、前提を疑うための認知の視点 / doubt-premise
minodriven
17
6.1k
Lemonade + Foundry Toolkit でお手軽アプリ開発
seosoft
1
310
AIで効率化できた業務・日常
ochtum
0
100
生成AI時代にこそ効くGo | Why Go Works in the Age of Generative AI
mom0tomo
8
3.1k
Featured
See All Featured
Side Projects
sachag
455
43k
Getting science done with accelerated Python computing platforms
jacobtomlinson
2
220
A Guide to Academic Writing Using Generative AI - A Workshop
ks91
PRO
1
320
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
17k
brightonSEO & MeasureFest 2025 - Christian Goodrich - Winning strategies for Black Friday CRO & PPC
cargoodrich
3
720
Money Talks: Using Revenue to Get Sh*t Done
nikkihalliwell
0
240
How to Get Subject Matter Experts Bought In and Actively Contributing to SEO & PR Initiatives.
livdayseo
0
130
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
71
40k
GraphQLとの向き合い方2022年版
quramy
50
15k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
6k
More Than Pixels: Becoming A User Experience Designer
marktimemedia
3
430
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Transcript
ViewController ⁶ Presenter by RxSwift iOS Test Night #3
ࣗݾհ • Takeshi Ihara / @nonchalant0303 • Recruit Marketing Partners
• iOS Engineer
RxSwift Rx is a generic abstraction of computation expressed through
Observable<Element> interface. This is a Swift version of Rx.
ViewController ⁶ Presenter
ViewController • ը໘දࣔϢʔβͷλονΠϕϯτͳͲͷ EventΛPresenterʹ௨͢Δ • Presenter͔Βड͚औͬͨStateʹΑΓView ͷදࣔΛΓସ͑Δ
Presenter • View͔ΒEventΛड͚औΓɺඞཁ͕͋Ε EventʹԠͨ͡UseCaseΛ࣮ߦ͢Δ • UseCase͔Βड͚औͬͨσʔλΛStateͱ͠ ͯView͢
ViewController (VC) Presenter State Event
prepare VC viewWillAppear Presenter → Event State ← prepared "1*$BMM
Refresh View touch touched %POPUIJOH Refresh View touchEvent
class Presenter { enum State { case initial case prepared(value:
Int) case touched } enum Event { case prepare case touch } let eventReciever = PublishSubject<Event>() private let innerViewRefrector = BehaviorSubject<State>(value: .initial) private let disposeBag = DisposeBag() var viewRefrector: Observable<State> { return innerViewRefrector.asObservable() } init() { eventReciever .flatMap { event -> Observable<State> in switch event { case .prepare: return [API Call] .map { .prepared(value: $0) } case .touch: return Observable.just(.touched) } } .bindTo(innerViewRefrector) .addDisposableTo(disposeBag) } }
class Presenter { enum State { // VCʹฦ͢ঢ়ଶ case initial
case prepared(value: Int) case touched } enum Event { // VC͔Βड͚औΔΠϕϯτ case prepare case touch } … }
class Presenter { … let eventReciever = PublishSubject<Event>() private let
innerViewRefrector = BehaviorSubject<State>(value: .initial) private let disposeBag = DisposeBag() var viewRefrector: Observable<State> { return innerViewRefrector.asObservable() } … } ௨ɾड৴ܥͷαϒδΣΫτ
class Presenter { … init() { eventReciever .flatMap { event
-> Observable<State> in switch event { case .prepare: return [API Call] .map { .prepared(value: $0) } case .touch: return Observable.just(.touched) } } .bindTo(innerViewRefrector) .addDisposableTo(disposeBag) } }
class Presenter { enum State { case initial case prepared(value:
Int) case touched } enum Event { case prepare case touch } let eventReciever = PublishSubject<Event>() private let innerViewRefrector = BehaviorSubject<State>(value: .initial) private let disposeBag = DisposeBag() var viewRefrector: Observable<State> { return innerViewRefrector.asObservable() } init() { eventReciever .flatMap { event -> Observable<State> in switch event { case .prepare: return [API Call] .map { .prepared(value: $0) } case .touch: return Observable.just(.touched) } } .bindTo(innerViewRefrector) .addDisposableTo(disposeBag) } } VCʹฦ͢ঢ়ଶ VC͔Βड͚औΔΠϕϯτ ௨ɾड৴ܥͷαϒδΣΫτ ΠϕϯτΛड͚औͬͯ ঢ়ଶΛฦ͢
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) } }
class ViewController: UIViewController { typealias Event = Presenter.Event var presenter:
Presenter! private let disposeBag = DisposeBag() override func viewDidLoad() { setupUIBindings() setupEventBindings() } … }
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) } … }
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) } }
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ΛૹΔ
prepare VC viewWillAppear Presenter → Event State ← prepared "1*$BMM
Refresh View touch touched %POPUIJOH Refresh View touchEvent
ViewController (VC) Presenter State Event
Test
ViewController (VC) Presenter State Event
ViewController (VC) Presenter State Event Testର
Test Case • Input: VC͔Βड͚औΔEvent • Output: Presenter͕VCʹૹΔState
prepare VC viewWillAppear Presenter → Event State ← prepared "1*$BMM
Refresh View touch touched %POPUIJOH Refresh View touchEvent
%POPUIJOH "1*$BMM VC viewWillAppear Presenter → Event State ← prepare
prepared Refresh View touch touched Refresh View touchEvent Test Case Test Case
Test with Rx • Rxσʔλ͕࣍ݩͱ͍͏୯ҐΛ͍࣋ͬͯΔ ͷͰɺ࣌ࠁʹΑͬͯҧ͏ঢ়ଶΛ͍࣋ͬͯΔ • ࣌ࠁʹґଘ͢Δςετڥґଘ͕େ͖͘ ෆ҆ఆͳςετʹͳΓ͍͢
RxTest • RxSwiftͷςετ༻ϑϨʔϜϫʔΫ • ࣮࣌ؒͱҟͳΔԾ࣌ؒʹج͖ͮΠϕϯ τΛൃੜͤ͞ΔΈ
// 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) }
Test Case
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))) } } } } }
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() } … } } }
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() … } } } } }
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))) } } } } } ड͚ͱͬͨঢ়ଶͷ ൃੜͨ࣌͠ࠁɾछྨΛݕূ
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))) } } } } } ड͚ͱͬͨঢ়ଶͷ ൃੜͨ࣌͠ࠁɾछྨΛݕূ Ծ࣌ࠁ্Ͱͷ ΠϕϯτϋϯυϦϯάΛઃఆ
ViewController (VC) Presenter State Event
·ͱΊ • ViewͷඳըͳͲʹґଘ͠ͳ͍γϯϓϧͳ ΠϕϯτϕʔεͷςετΛॻ͚ͨ • ·ͨɺෳͷΠϕϯτΛϋϯυϦϯά͢Δ ͜ͱͰෳࡶͳςετΛλΠϛϯάʹґଘ͞ ͤΔ͜ͱͳ͘ॻ͘͜ͱ͕ग़དྷΔ