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
Redux+Rxを活用したiOSアプリアーキテクチャ
Search
yohei sugigami
October 01, 2017
Technology
10
1.9k
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.1k
Redux with iOS
susieyy
0
1.2k
Why use Redux in iOS
susieyy
5
2.5k
ReduxRxを活用したアプリアーキテクチャ
susieyy
8
2.3k
Swaggerで始めるAPI定義管理とコードジェネレート
susieyy
14
7.4k
開発中のアプリをXcode9 & Swift4に移行しました
susieyy
0
3.6k
Wantedly People ViewModel and Rx
susieyy
7
6.9k
ReduxDevTools' power to the iOS development
susieyy
0
810
Realm Centered Design
susieyy
5
780
Other Decks in Technology
See All in Technology
TanStack Routerに移行するのかい しないのかい、どっちなんだい! / Are you going to migrate to TanStack Router or not? Which one is it?
kaminashi
0
530
dev 補講: プロダクトセキュリティ / Product security overview
wa6sn
1
2.3k
社内で最大の技術的負債のリファクタリングに取り組んだお話し
kidooonn
1
540
Amazon Personalizeのレコメンドシステム構築、実際何するの?〜大体10分で具体的なイメージをつかむ〜
kniino
1
100
OCI Security サービス 概要
oracle4engineer
PRO
0
6.5k
Terraform CI/CD パイプラインにおける AWS CodeCommit の代替手段
hiyanger
1
230
OCI Network Firewall 概要
oracle4engineer
PRO
0
4.1k
エンジニア人生の拡張性を高める 「探索型キャリア設計」の提案
tenshoku_draft
1
110
20241120_JAWS_東京_ランチタイムLT#17_AWS認定全冠の先へ
tsumita
2
220
ノーコードデータ分析ツールで体験する時系列データ分析超入門
negi111111
0
410
複雑なState管理からの脱却
sansantech
PRO
1
130
ExaDB-D dbaascli で出来ること
oracle4engineer
PRO
0
3.8k
Featured
See All Featured
Fantastic passwords and where to find them - at NoRuKo
philnash
50
2.9k
A Tale of Four Properties
chriscoyier
156
23k
Why You Should Never Use an ORM
jnunemaker
PRO
54
9.1k
Bash Introduction
62gerente
608
210k
The Illustrated Children's Guide to Kubernetes
chrisshort
48
48k
YesSQL, Process and Tooling at Scale
rocio
169
14k
Automating Front-end Workflow
addyosmani
1366
200k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
191
16k
Fontdeck: Realign not Redesign
paulrobertlloyd
82
5.2k
Teambox: Starting and Learning
jrom
133
8.8k
Agile that works and the tools we love
rasmusluckow
327
21k
Build The Right Thing And Hit Your Dates
maggiecrowley
33
2.4k
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ΞʔΩςΫνϟྑ͍Α!