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
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
640
Sign In with Apple
nonchalant
1
2.5k
iOSDC RejectCon 20180915: Factoryの自動生成によりテストを書きやすくする
nonchalant
1
750
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
Claude CodeとAgentCore Gatewayを繋ぐ際の認証認可 / Authentication and authorization when connecting Claude Code with AgentCore Gateway
har1101
2
380
リアルな遅延を測る仕様
kota_yata
1
130
「つくるAI」だけではバグは見つからない ~テストに必要な「見つけるAI」を分離させる戦略~
mfunaki
0
180
楽しそうなつよつよエンジニアと目が死んでる僕/A brilliant engineer having a blast, and dead-eyed me.
3l4l5
2
250
[PyCon KR 2026] More Variants, More Diversity for AI Accelerators
achimnol
0
110
<title><a id="</title>君はこのHTMLをパースできるか"></a></title> #雑LT_study
pizzacat83
0
180
FastAPI の並行処理モデルを完全に理解する
hoto17296
8
3k
Japan Community Day at Kubecon + CloudNativeCon Japan 2026: Learning Container Privilege Control by Building My Own Low-Level Container Runtime
ternbusty
1
170
まだ間に合う!今年の夏こそSchemeのマクロ展開器を完全理解!
omasanori
0
500
初めての模倣学習とVLA
natsutan
0
250
Pythonの実行はどこまで賢くなったのか? CPythonとPyPyから見る最適化のしくみ
curekoshimizu
3
1.9k
異なる設計思想のフレームワークを経験して得た学び
amekuhideki
1
550
Featured
See All Featured
[RailsConf 2023] Rails as a piece of cake
palkan
59
6.9k
Build your cross-platform service in a week with App Engine
jlugia
234
19k
How to build an LLM SEO readiness audit: a practical framework
nmsamuel
1
870
Everyday Curiosity
cassininazir
0
290
Skip the Path - Find Your Career Trail
mkilby
1
190
A Tale of Four Properties
chriscoyier
163
24k
Introduction to Domain-Driven Design and Collaborative software design
baasie
1
950
The Limits of Empathy - UXLibs8
cassininazir
1
610
Deep Space Network (abreviated)
tonyrice
0
270
Building Experiences: Design Systems, User Experience, and Full Site Editing
marktimemedia
0
580
JAMstack: Web Apps at Ludicrous Speed - All Things Open 2022
reverentgeek
1
580
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
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ͷඳըͳͲʹґଘ͠ͳ͍γϯϓϧͳ ΠϕϯτϕʔεͷςετΛॻ͚ͨ • ·ͨɺෳͷΠϕϯτΛϋϯυϦϯά͢Δ ͜ͱͰෳࡶͳςετΛλΠϛϯάʹґଘ͞ ͤΔ͜ͱͳ͘ॻ͘͜ͱ͕ग़དྷΔ