$30 off During Our Annual Pro Sale. View Details »
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Redux+Rxを活用したiOSアプリアーキテクチャ
Search
yohei sugigami
October 01, 2017
Technology
10
2.1k
Redux+Rxを活用したiOSアプリアーキテクチャ
俺コン Vol.1
2017/10/02@株式会社ディー・エヌ・エー
Yohei Suginami ( @susieyy )
yohei sugigami
October 01, 2017
Tweet
Share
More Decks by yohei sugigami
See All by yohei sugigami
Snapshot Testing in iOS
susieyy
6
3.3k
Redux with iOS
susieyy
0
1.3k
Why use Redux in iOS
susieyy
5
2.7k
ReduxRxを活用したアプリアーキテクチャ
susieyy
8
2.4k
Swaggerで始めるAPI定義管理とコードジェネレート
susieyy
14
7.6k
開発中のアプリをXcode9 & Swift4に移行しました
susieyy
0
3.7k
Wantedly People ViewModel and Rx
susieyy
7
7.3k
ReduxDevTools' power to the iOS development
susieyy
0
890
Realm Centered Design
susieyy
5
930
Other Decks in Technology
See All in Technology
WordPress は終わったのか ~今のWordPress の制作手法ってなにがあんねん?~ / Is WordPress Over? How We Build with WordPress Today
tbshiki
2
860
日本Rubyの会: これまでとこれから
snoozer05
PRO
4
200
AWSの新機能をフル活用した「re:Inventエージェント」開発秘話
minorun365
1
100
AWSに革命を起こすかもしれない新サービス・アップデートについてのお話
yama3133
0
210
AI駆動開発の実践とその未来
eltociear
1
400
Amazon Connect アップデート! AIエージェントにMCPツールを設定してみた!
ysuzuki
0
110
AI時代のワークフロー設計〜Durable Functions / Step Functions / Strands Agents を添えて〜
yakumo
3
1.4k
【U/Day Tokyo 2025】Cygames流 最新スマートフォンゲームの技術設計 〜『Shadowverse: Worlds Beyond』におけるアーキテクチャ再設計の挑戦~
cygames
PRO
2
940
マイクロサービスへの5年間 ぶっちゃけ何をしてどうなったか
joker1007
17
7k
Connection-based OAuthから学ぶOAuth for AI Agents
flatt_security
0
140
初めてのDatabricks AI/BI Genie
taka_aki
0
240
Jakarta Agentic AI Specification - Status and Future
reza_rahman
0
110
Featured
See All Featured
Writing Fast Ruby
sferik
630
62k
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.3k
StorybookのUI Testing Handbookを読んだ
zakiyama
31
6.5k
The Spectacular Lies of Maps
axbom
PRO
1
390
How to Build an AI Search Optimization Roadmap - Criteria and Steps to Take #SEOIRL
aleyda
1
1.8k
Unlocking the hidden potential of vector embeddings in international SEO
frankvandijk
0
120
Six Lessons from altMBA
skipperchong
29
4.1k
Exploring anti-patterns in Rails
aemeredith
2
200
How to Talk to Developers About Accessibility
jct
1
81
Building AI with AI
inesmontani
PRO
1
560
More Than Pixels: Becoming A User Experience Designer
marktimemedia
2
250
Agile Leadership in an Agile Organization
kimpetersen
PRO
0
46
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ΞʔΩςΫνϟྑ͍Α!