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
ReduxRxを活用したアプリアーキテクチャ
Search
yohei sugigami
January 25, 2018
Technology
2.4k
8
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
ReduxRxを活用したアプリアーキテクチャ
yohei sugigami
January 25, 2018
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
Redux+Rxを活用したiOSアプリアーキテクチャ
susieyy
10
2.2k
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
940
Realm Centered Design
susieyy
5
990
Other Decks in Technology
See All in Technology
紙 to デジタル
ichien178
0
130
巨大気象データと戦う ― サロゲートモデル学習を高速化する圧縮技術
gpuunite_official
0
270
VLMで2.3万枚のPyCon JP写真を検索!
terapyon
1
510
ハッカソンで入賞した話 @ Findy LT
asari194617
0
1.5k
アクセスキーが漏れた日にやるべきこと- 無効化の先にある本当の対応
kazzpapa3
0
320
Kiro WebとCloud Sessions
nagisa53
2
200
AI時代のデータ基盤を考える問い
pacocat
0
670
AIに持続⼒を与える 判断の⻑期記憶設計
eiei114
1
710
Microsoft MVP プログラムを紹介するから目指す人増えてくれ
tsubakimoto_s
0
110
平文パスワードはログに“残り” ── 肝心の侵入は“痕跡すら残らない”
kuroneko13
0
130
ClaudeCodeでセキュリティ監視業務を半自動化_1年の運用でわかったAIに任せる設計の5つのポイント
kintotechdev
3
340
AIで変わるエンジニアの働き方(仮)
naoinaoi
0
540
Featured
See All Featured
The Illustrated Guide to Node.js - THAT Conference 2024
reverentgeek
1
440
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
Ten Tips & Tricks for a 🌱 transition
stuffmc
0
180
Leo the Paperboy
mayatellez
8
2.2k
We Analyzed 250 Million AI Search Results: Here's What I Found
joshbly
1
1.9k
Lessons Learnt from Crawling 1000+ Websites
charlesmeaden
PRO
1
1.5k
Agile Leadership in an Agile Organization
kimpetersen
PRO
0
210
The Invisible Side of Design
smashingmag
301
52k
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.8k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
4.5k
Side Projects
sachag
455
43k
Site-Speed That Sticks
csswizardry
13
1.5k
Transcript
Redux+RxΛ׆༻ͨ͠ΞϓϦΞʔ ΩςΫνϟ ɹ CA.swi! #5 2018/01/25@גࣜձࣾαΠόʔΤʔδΣϯτ 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) ͱ ୯ํͷσʔλϑϩʔ(Unidirectional Data Flow)
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 Viewͱঢ়ଶͷશͳΔ ViewͷϩδοΫͦͷॠؒͷΞϓϦͷͯ͢ͷঢ়ଶ͕ҰҙͰ ͋Δ͜ͱΛอূ͞ΕΔ ʢ ͦͷॠؒͷঢ়ଶʹରͯ͠ඇಉظͷհ ೖ͕ͳ͍ ʣ ඇಉظ࣮ߦதͷதؒঢ়ଶҙࣝ͢Δඞཁ͕ͳ͍ɺγϯάϧϝΠ ϯεϨου͚ͩͰಈ࡞͍ͯ͠ΔΑ͏ͳִؒͰϩδοΫ͕ॻ͚Δ
POINT Πϛϡʔλϒϧͱ७ਮؔΛ׆༻ͯ͠పఈతʹ෭࡞༻ͷഉআ ʢʣʹప͍ͯ͠Δ͜ͱ͕ɺଞͷMVC,MVVM,CleanͳͲͷ ΞʔΩςΫνϟͱҟͳΔઃܭࢥͰ͋ΓɺγϯϓϧͰݎ࿚Ͱ ͭՄಡੑͷߴ͍ίʔσΟϯάΛՄೳͱ͢Δݯઘ Πϛϡʔλϒϧ͕ѻ͑ͯؔܕࢦͳSwiftͱ૬ੑ͕Α͍
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) } } } }
RxDatasource — https://github.com/RxSwiftCommunity/RxDataSources — O(N) algorithm for calculating differences —
the algorithm has the assumption that all sections and items are unique so there is no ambiguity — in case there is ambiguity, fallbacks automagically on non animated refresh
None
ɹ ɹ One more thing !
ɹ ɹ ReduxDebug͕͍͢͠
ɹ ɹ ReduxDevToolsͰঢ়ଶͷϞχλϦϯά
ɹ ɹ DEMO
Conclusion ɹ ɹ ReduxΞʔΩςΫνϟྑ͍Α!