Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
iOS Test Night #3: ViewController ⇄ State by Rx...
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
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.4k
iOSDC 20190906: 動画アプリの投げ銭機能における 消耗型課金の仕組みと実装 with 発表ノート
nonchalant
2
650
Sign In with Apple
nonchalant
1
2.5k
iOSDC RejectCon 20180915: Factoryの自動生成によりテストを書きやすくする
nonchalant
1
760
iOSDC 20180902: 小さくはじめる端末管理
nonchalant
2
1k
devsap 20180728: コード生成のススメ
nonchalant
0
150
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
AIエージェント時代のコードレビューを設計する
nogu66
6
2.7k
AI に Inclusive UI を書かせよう — Design Rules Skill で Compose UI を作り直す
theoriatec2024
1
490
iOSDC2026登壇資料.pdf
riofujimon
0
160
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
370
スマート反転とウェブアクセシビリティ
camiha
0
210
LoopHub - ローカルで動く GitHub で、AI と共同開発
jugyo
1
550
cdk deploy JawsSonic #MARATHONしながらAWSリソースをデプロイしてみよう
akihisaikeda
2
130
手動確認はもう限界 〜XCUITestでCustom URL Schemeの遷移を起動種別ごとに自動テストする〜 / Testing Custom URL Schemes with XCUITest
otouto
0
280
What We Talk About When We Talk About XP
m_seki
2
620
SONY CISC-NEWS NWS-1750 + NWB-225 フレームバッファの NetBSD/news68k ドライバ実装 / OSC2026Hiroshima
tsutsui
0
110
一人だけ、Kiroが静止する日
hideg
0
110
海上で動くGoサーバー: goroutineとchannelでさばく航行データストリーム
atsuki_seo
0
690
Featured
See All Featured
Making the Leap to Tech Lead
cromwellryan
135
10k
Joys of Absence: A Defence of Solitary Play
codingconduct
1
520
Ruling the World: When Life Gets Gamed
codingconduct
0
330
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
500
Mind Mapping
helmedeiros
1
360
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
10
1.3k
Why Your Marketing Sucks and What You Can Do About It - Sophie Logan
marketingsoph
0
410
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
540
Balancing Empowerment & Direction
lara
6
1.3k
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Effective software design: The role of men in debugging patriarchy in IT @ Voxxed Days AMS
baasie
1
520
Scaling GitHub
holman
464
140k
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ͷඳըͳͲʹґଘ͠ͳ͍γϯϓϧͳ ΠϕϯτϕʔεͷςετΛॻ͚ͨ • ·ͨɺෳͷΠϕϯτΛϋϯυϦϯά͢Δ ͜ͱͰෳࡶͳςετΛλΠϛϯάʹґଘ͞ ͤΔ͜ͱͳ͘ॻ͘͜ͱ͕ग़དྷΔ