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
Redux+Rxを活用したiOSアプリアーキテクチャ
Search
yohei sugigami
October 01, 2017
Technology
2.2k
10
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Redux+Rxを活用したiOSアプリアーキテクチャ
俺コン Vol.1
2017/10/02@株式会社ディー・エヌ・エー
Yohei Suginami ( @susieyy )
yohei sugigami
October 01, 2017
More Decks by yohei sugigami
See All by yohei sugigami
Snapshot Testing in iOS
susieyy
6
3.4k
Redux with iOS
susieyy
0
1.4k
Why use Redux in iOS
susieyy
5
2.8k
ReduxRxを活用したアプリアーキテクチャ
susieyy
8
2.4k
Swaggerで始めるAPI定義管理とコードジェネレート
susieyy
14
7.8k
開発中のアプリをXcode9 & Swift4に移行しました
susieyy
0
3.8k
Wantedly People ViewModel and Rx
susieyy
7
7.6k
ReduxDevTools' power to the iOS development
susieyy
0
950
Realm Centered Design
susieyy
5
1k
Other Decks in Technology
See All in Technology
aws-iot-platform-architecture-use-cases.pdf
ma2shita
0
140
空間オーディオで過去の 自分(ゴースト)と競うランニング 〜HealthKitのルートを足音に変える実装〜
nao_randd
0
220
アプリログインとWeb認証基盤をつなぐ ASWebAuthenticationSession 作法
shimastripe
1
320
DEFCON_CHV_CTF_Write-up.pdf
bata_24
0
150
日経電子版を支えていく Kasane Design System/fec_fukuoka
nikkei_engineer_recruiting
0
1.5k
AIエージェントの自己改善をどう設計するか / How to Design Self-Improvement for AI Agents
22mi
23
15k
Railsのように考える: See through the Master
snoozer05
PRO
3
850
Minecraft JavaのMODをSwiftで作る
1mash0
0
160
Genieを崇めよ
kameitomohiro
0
130
LTのテーマ どうきめてる?〜5つの型と私のやり方〜
yama3133
1
100
AI de Idea
kawaguti
PRO
2
110
OpenTelemetryのメトリクスをCloudWatchに送ってPromQLで見てみた
ota1022
0
150
Featured
See All Featured
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
660
We Analyzed 250 Million AI Search Results: Here's What I Found
joshbly
1
1.9k
Building a Scalable Design System with Sketch
lauravandoore
464
34k
Breaking role norms: Why Content Design is so much more than writing copy - Taylor Woolridge
uxyall
1
410
Optimizing for Happiness
mojombo
378
71k
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
1
390
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
The Art of Programming - Codeland 2020
erikaheidi
57
14k
Agile that works and the tools we love
rasmusluckow
331
22k
<Decoding/> the Language of Devs - We Love SEO 2024
nikkihalliwell
1
330
How Fast Is Fast Enough? [PerfNow 2025]
tammyeverts
3
880
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.6k
Transcript
Redux+RxΛ׆༻ͨ͠ΞϓϦΞʔ ΩςΫνϟ ɹ Զίϯ Vol.1 2017/10/02@גࣜձࣾσΟʔɾΤψɾΤʔ Yohei Suginami ( @susieyy
)
Profile — Yohei Sugigami — susieyy — Twitter / Qiita
/ Github — New App Development Specialization — Clients — Folio.inc — New app developer — Wantedly.inc — Technical advisor
ɹ ɹ Reduxͱ Reduxͷ3ͭͷݪଇ(Redux Three Principles)
Single source of truth ɹ State is read-only ɹ Mutations
are wri!en as pure functions
Single source of truth / ৴པͰ͖Δ།Ұͷঢ়ଶ ΞϓϦશମͷঢ়ଶΛ̍ͭͷΦϒδΣΫτπϦʔͰදݱ
State is read-only / ঢ়ଶΠϛϡʔλϒϧ มߋ͢ΔʹඞͣΞΫγϣϯΛൃߦ͢Δ
Mutations are wri!en as pure functions / ७ਮؔ ঢ়ଶมߋ෭࡞༻͕ͳ͍७ਮؔΛ༻͍ɺঢ়ଶΛมߋͤ ͣɺ৽͍͠ঢ়ଶΛ࡞ͬͯฦ͢
ɹ ɹ ୯ํͷσʔλϑϩʔ Unidirectional Data Flow
None
POINT ༧ଌՄೳͳܗͰίʔυΛએݴతʹߏԽ͢Δ͜ͱ - ༧ଌՄೳ - ঢ়ଶΛҰݩతʹཧ - ঢ়ଶมԽγʔέϯγϟϧ - ෭࡞༻ͱͷ
- એݴత - ঢ়ଶมԽͷىҼ͕໌ࣔత ʢ ActionΛDispatch ʣ - ঢ়ଶมԽ७ਮؔ
ReduxΛͳʹͰ࣮͢Δ͔ — ຊՈReduxͷεςοϓ318ͱඇৗʹগͳ͍ — ࣮ΛࢀߟʹࣗͰϙʔςΟϯά͢Δ͜ͱՄೳͳྔ — ϥΠϒϥϦ͍͔࣮ͭ͘͞Ε͍ͯΔͷͰݕ౼ͯ͠ΈΔ
ReduxܥϥΠϒϥϦ — ReSwift ˒4,247 — ReactiveReSwift ˒71 — KATANA ˒
1,602 — ReduxKit ˒583 — Reactor ˒129
FluxܥϥΠϒϥϦ — Dispatch ˒239 — SwiftFlux ˒212 — FluxWithRxSwiftSample ˒109
— FluxxKit ˒33
ݕ౼݁Ռ — ReSwift͕Ұ൪༗໊ — ؔΫϩʔδϟʔΛத৺ʹઃܭ͞Ε͓ͯΓɺ࣮͕ͱͯ ៉ྷ — ϦϦʔεස΄ͲΑ͘ɺISSUE׆ൃ
ɹ ɹ ReSwi!ͱ ReSwi!ͷ࣮ͱར༻ྫΛݟͯΈΔ
State Action Reducer Dispatch ActionCreator
None
State protocol StateType { }
State / e.g. struct AppState: ReSwift.StateType { var timelineState =
TimelineState() var userProfileState = UserProfileState() } struct TimelineState: ReSwift.StateType { var tweets: [Tweet] var response: [Tweet] }
State — Structͷߏ — ֤StructReSwi!.StateTypeϓϩτίϧʹ४ڌ͢Δ — ׳ྫతʹҰ൪্ͷঢ়ଶΛද͢StructΛAppStateͱ͢Δ
None
Action protocol Action { }
Action / e.g. extension TimelineState { enum Action: ReSwift.Action {
case requestSuccess(response: [Tweet]) case requestState(fetching: Bool) case requestError(error: Error) } }
Action — ReSwi!.Actionϓϩτίϧʹ४ڌ͍ͯ͠ΕɺStructͰ ɺEnumͰΑ͍ — ॲཧΛ༗͠ͳ͍ͨͩͷσʔλ — ReducerͰͲ͏͍͏ॲཧΛ͍͔ͨ͠ͷछྨͱͦͷΠϯϓο τσʔλʹͳΔ
None
Reducer typealias Reducer<ReducerStateType> = (action: Action, state: ReducerStateType?) -> ReducerStateType
Reducer / e.g extension TimelineState { public static func reducer(action:
ReSwift.Action, state: TimelineState?) -> TimelineState { var state = state ?? TimelineState() guard let action = action as? TimelineState.Action else { return state } switch action { case let .requestState(fetching): state.fetching = fetching state.error = nil case let .requestSuccess(response): state.fetching = false state.response = response state.dataSourceElements = DataSourceElements(response.map({ DiffableWrap($0) })) case let .requestError(error): state.fetching = false state.error = error } return state } }
Reducer — ReduxͰঢ়ଶมߋͰ͖ΔͷReducer͚ͩͱ͍͏੍ — Reducer (state, action) => state Λຬͨ͢ঢ়ଶΛ࣋ͨ
ͳ͍;ͭ͏ͷؔʢ७ਮؔʣͰͳ͚ΕͳΒͳ͍
Reducer / Initialization func appReduce(action: ReSwift.Action, state: AppState?) -> AppState
{ var state = state ?? AppState() state.timelineState = TimelineState.reducer( action: action, state: state.timelineState) state.userProfile = UserProfileState.reducer( action: action, state: state.userProfile) return state } var appStore = ReSwift.Store<AppState>( reducer: appReduce, state: nil, middleware: [])
Reducer / Initialization — ׳ྫతʹҰ൪্ͷঢ়ଶΛද͢StructΛappReduceͱ͢Δ — appReduceΛىʹɺԼҐͷReducerʹActionΛϒϩʔυ Ωϟετ͢Δ
None
Store open class Store<State: ReSwift.StateType>: ReSwift.StoreType { var state: State!
{ get } private var reducer: Reducer<State> open func dispatch(_ action: Action) { ... } open func subscribe<S: StoreSubscriber>(_ subscriber: S) { ... } open func unsubscribe(_ subscriber: AnyStoreSubscriber) { ... } ... }
Store — StateͱReducerΛอ࣋͢Δγϯάϧτϯ — ActionͷσΟεύονϝιουΛ༗͢Δ — StateͷαϒεΫϥΠϒϝιουΛ༗͢Δ
None
Dispatch / e.g. let action = TimelineState.Action.requestState(fetching: true) appStore.dispatch(action)
Dispatch — ViewଆͰActionͷΠϯελϯεΛ࡞͠ɺStoreͷ DispatchϝιουΛίʔϧ͢Δ
None
ActionCreator typealias ActionCreator = (state: ReSwift.State, store: ReSwift.StoreType) -> ReSwift.Action?
ActionCreator — DispatchՄೳͳؔͰActionΛฦ͢ — ActionCreatorؔͰStateʹΞΫηεՄೳͰɺState ΛՃͯ͠ActionΛ࡞͢Δ༻్ʹར༻Ͱ͖Δ — ActionCreatorؔͰStoreʹΞΫηεՄೳͰɺ DispatchϝιουΛίʔϧ͢Δ͜ͱͰ͖Δ
ɹ ɹ ReSwi! Rx׆༻ฤ ෭࡞༻ʢඇಉظ௨৴ʣ/ ViewDataBinding
෭࡞༻ʢඇಉظ௨৴ʣ/ Asynchronous Operations ReSwi!ͷREADMEʹΑΔඇಉظ௨৴ͷྫ ActionCreatorͰ෭࡞༻ʢඇಉظ௨৴ʣΛߦ͍ͬͯΔ func fetchGitHubRepositories(state: State, store: Store<State>)
-> Action? { guard case let .LoggedIn(configuration) = state.authenticationState.loggedInState else { return nil } Octokit(configuration).repositories { response in dispatch_async(dispatch_get_main_queue()) { store.dispatch(SetRepostories(repositories: response)) } } return nil }
෭࡞༻ʢඇಉظ௨৴ʣ/ Asynchronous Operations — ActionCreator͕ؔ७ਮؔͰͳ͘ͳΔ — ؔʹ෭࡞༻ʢඇಉظ௨৴ʣ͕͋Δͱςετ͕͠ʹ͍͘ — ඒ͘͠ͳ͍(ݸਓͷײͰ͢) —
ͱ͍͑ɺReducer७ਮؔͰ෭࡞༻Λڐ༰͠ͳ͍ͷ ͰɺReducerʹهड़Ͱ͖ͳ͍
ɹ ɹ Redux(JS)Ͱ MiddlewareͰ෭࡞༻(ඇಉظॲཧ)Λѻ͏
None
Middleware public typealias DispatchFunction = (Action) -> Void public typealias
Middleware<State> = ( @escaping DispatchFunction, @escaping () -> State? ) -> (@escaping DispatchFunction) -> DispatchFunction
Middleware / e.g. let loggingMiddleware: ReSwift.Middleware<AppState> = { dispatch, getState
in return { next in return { action in logger.info("! [Action] \(action)") return next(action) } } var appStore = Store<AppState>( reducer: appReduce, state: nil, middleware: [loggingMiddleware] )
ɹ ɹ Redux(JS)ͷ ඇಉظ༻Middlewareͨͪ
refs. Redux Middleware ɹ redux-thunk redux-sage redux-promise
Redux thunkͷιʔείʔυʢશྔʣ function createThunkMiddleware(extraArgument) { return ({ dispatch, getState })
=> next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
ɹ ɹ ;Ή;Ή!
ɹ ɹ RxSwi! ׆༻ͯ͠ඇಉظॲཧΛMiddlewareʹԡ͠ࠐΉ
rxThunkMiddleware struct SingleAction: ReSwift.Action { public let single: Single<ReSwift.Action> public
let disposeBag: DisposeBag } let rxThunkMiddleware: ReSwift.Middleware<AppState> = { dispatch, getState in return { next in return { action in if let action = action as? SingleAction { action.single .observeOn(MainScheduler.instance) .subscribe(onSuccess: { next($0) }) .disposed(by: action.disposeBag) } else { return next(action) } } } }
rxThunkMiddleware / e.g. func requstAsyncCreator(maxID: String) -> Store<AppState>.ActionCreator { return
{ (state: AppState, store: Store<AppState>) in if state.timelineState.fetching { return nil } let s = TwitterManager.shared.timeline(maxID: maxID) .map { return Timeline.Action.requestSuccess(response: $0) } .catchError { let action = Timeline.Action.requestError(error: $0) return Single<ReSwift.Action>.just(action) } return SingleAction(single: s, disposeBag: state.timelineState.requestDisposeBag) } }
ɹ ɹ Testability
ɹ ɹ ReduxͷੈքΠϛϡʔλϒϧ ७ਮؔɺ෭࡞༻ͷΈͰߏங͞ΕΔ
— Πϛϡʔλϒϧෆม — ActionʢΠϯϓοτύϥϝʔλʔʣɺStateʢσʔλʣ — ७ਮؔςετ͕༰қ — ҙͷΠϯϓοτʹରͯ͠Ξτϓοτ͕Ұҙʹܾ·Δ — ActionCreatorͱReducer
— ෭࡞༻ςετ͕ෳࡶ — Middlewareͷ෦ςετ࣌ʹελϒʹࠩ͠ସ͑
None
ɹ ɹ DIෆཁ! Dependency Injection
ɹ ɹ ViewDataBinding
ɹ ɹ ReSwi!#subscribe
override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) store.subscribe(self) { subcription in
subcription.select { state in state.repositories } } } override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) store.unsubscribe(self) } func newState(state: Response<[Repository]>?) { if case let .Success(repositories) = state { dataSource?.array = repositories tableView.reloadData() } }
ɹ ɹ RxSwi! ReSwi!.StoreΛRxSwi!.Variableʹม
let rxReduxStore = RxReduxStore<AppState>(store: appStore) public class RxReduxStore<AppStateType>: StoreSubscriber where
AppStateType: StateType { public lazy var stateObservable: Observable<AppStateType> = { return self.stateVariable.asObservable().observeOn(MainScheduler.instance) .shareReplayLatestWhileConnected() }() public var state: AppStateType { return stateVariable.value } private let stateVariable: Variable<AppStateType> private let store: Store<AppStateType> public init(store: Store<AppStateType>) { self.store = store self.stateVariable = Variable(store.state) self.store.subscribe(self) } deinit { self.store.unsubscribe(self) } public func newState(state: AppStateType) { self.stateVariable.value = state } public func dispatch(_ action: Action) { store.dispatch(action) } public func dispatch(_ actionCreatorProvider: @escaping (AppStateType, ReSwift.Store<AppStateType>) -> Action?) { store.dispatch(actionCreatorProvider) } }
class TimeLineViewController: UIViewController { fileprivate let disposeBag = DisposeBag() fileprivate
let rxReduxStore: RxReduxStore<AppState> init(_ rxReduxStore: RxReduxStore<AppState>) { self.rxReduxStore = rxReduxStore super.init(nibName: nil, bundle: nil) } override func viewDidLoad() { super.viewDidLoad() rxReduxStore.stateObservable .map { $0.timelineState.dataSourceElements } .bind(to: adapter.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) rxReduxStore.stateObservable .map { $0.timelineState.fetching } .distinctUntilChanged() .bind(to: loadingView.rx.fetching) .disposed(by: disposeBag) } }
ɹ ɹ Viewͷࠩߋ৽
None
ɹ ɹ ReactͰ
ɹ ɹ Virtual DOM
None
ɹ ɹ iOSͰ
ɹ ɹ IGListKit1 1 https://github.com/Instagram/IGListKit
ɹ ɹ σʔλࠩΞϧΰϦζϜʹΑΔߴࠩߋ৽
ɹ ɹ ίϯϙʔωϯτࢦͰଟ༷ͳViewΛѻ͑Δ
None
— UICollectionViewΛϕʔεʹͰ͖͍ͯΔ — σʔλ͍ΖΜͳܕͷཁૉΛؚΉ̍࣍ݩྻ — σʔλͷཁૉͷܕͰίϯϙʔωϯτΛذ͢Δ — σʔλͷཁૉൺֱతՄೳͳͨΊʹIGListKitͷϓϩτίϧ ʹ४ڌ͢Δ(EquitableΈ͍ͨͳͷ) —
ObjCͳͱ͜Ζ͕൵͍͠! — ϓϩτίϧ४ڌͷͨΊʹNSObjectΛܧঝ͕ඞཁ
final class TimeLineViewController: UIViewController { fileprivate let dataSource = DataSource()
fileprivate lazy var adapter: ListAdapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self) fileprivate let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.rx .setDataSource(dataSource) .disposed(by: disposeBag) rxReduxStore.stateObservable .map { $0.timelineState.dataSourceElements } .bind(to: adapter.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) } } extension TimeLineViewController { fileprivate final class DataSource: AdapterDataSource { override func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { switch object { case let o as DiffableWrap<Tweet>: return TweetsSectionController(o) case let o as DiffableWrap<[RecommendUser]>: return RecommendUsersSectionController(o) } } } }
ɹ ɹ One more thing !
ɹ ɹ ReduxDebug͕͍͢͠
ɹ ɹ ReduxDevToolsͰঢ়ଶͷϞχλϦϯά
ɹ ɹ DEMO
Conclusion ɹ ɹ ReduxΞʔΩςΫνϟྑ͍Α!