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
·
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.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
JAWS-UG 東京支部が始める、JAWS-UG支部コラボ / JAWS-UG lunchtime LT Collaboration
y0hgi
0
170
難しいけど、読めた。- OSSの入口に立った話。
sts11142
0
130
Omarchy Tokyo やると聞いて UMPC 買ってセットアップしてきた
mtsmfm
0
190
wkhtmltopdfの次どうするか問題2026
willnet
2
1.8k
パズルゲームの作り方 / how to make puzzle games
kaityo256
PRO
2
250
Ghostty + Neovimで作る 透明でカッコ良い開発環境
j341nono
0
140
SONY CISC-NEWS NWS-1750 + NWB-225 フレームバッファの NetBSD/news68k ドライバ実装 / OSC2026Hiroshima
tsutsui
0
160
Findy - エンジニア向け会社紹介/Findy Company Deck
findyinc
6
400k
海上で動くGoサーバー: goroutineとchannelでさばく航行データストリーム
atsuki_seo
0
1k
FreeBSDでZabbixを動かす
kenkino
0
340
手動確認はもう限界 〜XCUITestでCustom URL Schemeの遷移を起動種別ごとに自動テストする〜 / Testing Custom URL Schemes with XCUITest
otouto
0
360
20260914 AIエージェント時代のPlatform Engineering LLM基盤とプロダクトの責務境界線
kanfab1
7
2.3k
Featured
See All Featured
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
1
3.2k
Automating Front-end Workflow
addyosmani
1369
210k
What Being in a Rock Band Can Teach Us About Real World SEO
427marketing
0
1.1k
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.6k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
62k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
28
3.7k
Gemini Prompt Engineering: Practical Techniques for Tangible AI Outcomes
mfonobong
2
560
The Organizational Zoo: Understanding Human Behavior Agility Through Metaphoric Constructive Conversations (based on the works of Arthur Shelley, Ph.D)
kimpetersen
PRO
0
480
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
1
410
Paper Plane
katiecoart
PRO
4
53k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.3k
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ͷඳըͳͲʹґଘ͠ͳ͍γϯϓϧͳ ΠϕϯτϕʔεͷςετΛॻ͚ͨ • ·ͨɺෳͷΠϕϯτΛϋϯυϦϯά͢Δ ͜ͱͰෳࡶͳςετΛλΠϛϯάʹґଘ͞ ͤΔ͜ͱͳ͘ॻ͘͜ͱ͕ग़དྷΔ