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.4k
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
メールのエイリアス機能を履き違えない
isshinfunada
0
220
Built Our Own Background Agent at LayerX #aidevex_findy
layerx
PRO
9
4.8k
AI Engineeringは、AIプロダクトだけのものか? 〜AIがソフトウェアを作る時代の新しい当たり前〜 / No AI in your product. AI Engineering in your development.
rkaga
4
360
20260722_microCMSで考える、AI時代のコンテンツ運用設計
yosh1
0
310
為什麼你並不需要ViewModel / No, you don't need a ViewModel
lovee
1
480
AWS DevOps AgentのAzure接続機能を検証して見えた活用法/Use Cases Verified for the AWS DevOps Agent's Azure Connectivity Feature
masakiokuda
1
210
freee が目指す データ マネジメント戦略 AI-Ready 時代を支える 攻めのガバナンスとは
freee
PRO
0
240
yield再入門 #phpcon
o0h
PRO
0
930
Google Apps Script で Ruby を動かす
kawahara
0
130
数百円から始めるRuby電子工作
tarosay
0
130
AI時代、エンジニアはどう育つのか -未経験エンジニアの成長を間近で見て考えたこと-
thasu0123
0
210
変わらないものが、変わるものを決める — 意図駆動開発 × イベントソーシング × イミュータブル | What Doesn't Change Decides What Can — IDD × Event Sourcing × Immutability
tomohisa
0
1.2k
Featured
See All Featured
Highjacked: Video Game Concept Design
rkendrick25
PRO
1
420
brightonSEO & MeasureFest 2025 - Christian Goodrich - Winning strategies for Black Friday CRO & PPC
cargoodrich
3
760
Ten Tips & Tricks for a 🌱 transition
stuffmc
0
160
KATA
mclloyd
PRO
35
15k
Marketing Yourself as an Engineer | Alaka | Gurzu
gurzu
0
270
How to make the Groovebox
asonas
2
2.3k
GitHub's CSS Performance
jonrohan
1033
470k
The agentic SEO stack - context over prompts
schlessera
0
860
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Producing Creativity
orderedlist
PRO
348
40k
SEO for Brand Visibility & Recognition
aleyda
0
4.7k
30 Presentation Tips
portentint
PRO
1
360
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ͷඳըͳͲʹґଘ͠ͳ͍γϯϓϧͳ ΠϕϯτϕʔεͷςετΛॻ͚ͨ • ·ͨɺෳͷΠϕϯτΛϋϯυϦϯά͢Δ ͜ͱͰෳࡶͳςετΛλΠϛϯάʹґଘ͞ ͤΔ͜ͱͳ͘ॻ͘͜ͱ͕ग़དྷΔ