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
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
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.7k
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
930
Realm Centered Design
susieyy
5
990
Other Decks in Technology
See All in Technology
Kaggleで成長するために意識したこと
prgckwb
2
400
AIコード生成×サプライチェーン攻撃 — PHPが直面する“二重の信頼問題
shinyasaita
0
160
穢れた技術選定について
watany
17
5.2k
10年目を迎えた「ABEMA」がどのように AI 活用を推進して、AI 駆動開発にシフトしているのか / How ABEMA, entering its 10th year, is promoting the use of AI and shifting toward AI-driven development
miyukki
0
210
LLMやAIエージェントをソフトウェアに組み込むプラクティス
shibuiwilliam
2
400
AIと共生する開発者プラットフォーム:バクラクのモノレポ×マイクロサービス基盤
sakajunquality
2
3.7k
ガバナンスの「ちょうどいい落とし所」を探れ!開発スピードを妨げない運用判断の勘所 / SRE NEXT 2026
genda
1
200
Claude Code公式skillで 自分の仕事を少しずつ手放そう!(Claude Code開発ノウハウ大公開スペシャル by クラスメソッド)
kaym
1
450
しぶいSRE: サーバから見えない障害にどう向き合うか。ラストワンマイルのデバッグ実践 / Shibui SRE
kanny
13
6.3k
Type-safe IaC for Dart
coborinai
0
110
企業でAWS Organizationsを動かすための組織設計の考え方
nrinetcom
PRO
1
100
SRE本の知られざる名シーン / The Hidden Gems of Google SRE Book
nari_ex
1
410
Featured
See All Featured
Claude Code どこまでも/ Claude Code Everywhere
nwiizo
65
56k
Prompt Engineering for Job Search
mfonobong
0
370
Making the Leap to Tech Lead
cromwellryan
135
10k
The Curse of the Amulet
leimatthew05
2
13k
The Limits of Empathy - UXLibs8
cassininazir
1
490
Building a Modern Day E-commerce SEO Strategy
aleyda
45
9.1k
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
200
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
17k
Discover your Explorer Soul
emna__ayadi
2
1.2k
My Coaching Mixtape
mlcsv
0
170
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
360
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.3k
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ΞʔΩςΫνϟྑ͍Α!