Slide 1

Slide 1 text

Redux+RxΛ׆༻ͨ͠ΞϓϦΞʔ ΩςΫνϟ ɹ CA.swi! #5 2018/01/25@גࣜձࣾαΠόʔΤʔδΣϯτ Yohei Suginami ( @susieyy )

Slide 2

Slide 2 text

Profile — Yohei Sugigami — susieyy — Twitter / Qiita / Github — New App Development Specialization — Clients — Folio.inc — New app developer — Wantedly.inc — Technical advisor

Slide 3

Slide 3 text

ɹ ɹ Reduxͱ͸ Reduxͷ3ͭͷݪଇ(Redux Three Principles) ͱ ୯ํ޲ͷσʔλϑϩʔ(Unidirectional Data Flow)

Slide 4

Slide 4 text

Single source of truth ɹ State is read-only ɹ Mutations are wri!en as pure functions

Slide 5

Slide 5 text

Single source of truth / ৴པͰ͖Δ།Ұͷঢ়ଶ ΞϓϦશମͷঢ়ଶΛ̍ͭͷΦϒδΣΫτπϦʔͰදݱ

Slide 6

Slide 6 text

State is read-only / ঢ়ଶ͸Πϛϡʔλϒϧ มߋ͢Δʹ͸ඞͣΞΫγϣϯΛൃߦ͢Δ

Slide 7

Slide 7 text

Mutations are wri!en as pure functions / ७ਮؔ਺ ঢ়ଶมߋ͸෭࡞༻͕ͳ͍७ਮؔ਺Λ༻͍ɺঢ়ଶΛ௚઀มߋͤ ͣɺ৽͍͠ঢ়ଶΛ࡞ͬͯฦ͢

Slide 8

Slide 8 text

ɹ ɹ ୯ํ޲ͷσʔλϑϩʔ Unidirectional Data Flow

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

POINT Viewͱঢ়ଶͷ׬શͳΔ෼཭ ViewͷϩδοΫ͸ͦͷॠؒͷΞϓϦ಺ͷ͢΂ͯͷঢ়ଶ͕ҰҙͰ ͋Δ͜ͱΛอূ͞ΕΔ ʢ ͦͷॠؒͷঢ়ଶʹରͯ͠ඇಉظͷհ ೖ͕ͳ͍ ʣ ඇಉظ࣮ߦதͷதؒঢ়ଶ΋ҙࣝ͢Δඞཁ͕ͳ͍ɺγϯάϧϝΠ ϯεϨου͚ͩͰಈ࡞͍ͯ͠ΔΑ͏ͳִؒͰϩδοΫ͕ॻ͚Δ

Slide 11

Slide 11 text

POINT Πϛϡʔλϒϧͱ७ਮؔ਺Λ׆༻ͯ͠పఈతʹ෭࡞༻ͷഉআ ʢ෼཭ʣʹప͍ͯ͠Δ͜ͱ͕ɺଞͷMVC,MVVM,CleanͳͲͷ ΞʔΩςΫνϟͱҟͳΔઃܭࢥ૝Ͱ͋ΓɺγϯϓϧͰݎ࿚Ͱ׌ ͭՄಡੑͷߴ͍ίʔσΟϯάΛՄೳͱ͢Δݯઘ Πϛϡʔλϒϧ͕ѻ͑ͯؔ਺ܕࢦ޲ͳSwiftͱ΋૬ੑ͕Α͍

Slide 12

Slide 12 text

POINT ༧ଌՄೳͳܗͰίʔυΛએݴతʹߏ଄Խ͢Δ͜ͱ - ༧ଌՄೳ - ঢ়ଶΛҰݩతʹ؅ཧ - ঢ়ଶมԽ͸γʔέϯγϟϧ - ෭࡞༻ͱͷ෼཭(ඇಉظతมԽͷഉআ) - એݴత - ঢ়ଶมԽͷىҼ͕໌ࣔత ʢ ActionΛDispatch ʣ - ঢ়ଶมԽ͸७ਮؔ਺

Slide 13

Slide 13 text

ReduxΛͳʹͰ࣮૷͢Δ͔ — ຊՈReduxͷεςοϓ਺͸318ͱඇৗʹগͳ͍ — ࣮૷Λࢀߟʹࣗ෼ͰϙʔςΟϯά͢Δ͜ͱ΋Մೳͳ෼ྔ — ϥΠϒϥϦ΋͍͔࣮ͭ͘૷͞Ε͍ͯΔͷͰݕ౼ͯ͠ΈΔ

Slide 14

Slide 14 text

ReduxܥϥΠϒϥϦ — ReSwift ˒4,247 — ReactiveReSwift ˒71 — KATANA ˒ 1,602 — ReduxKit ˒583 — Reactor ˒129

Slide 15

Slide 15 text

FluxܥϥΠϒϥϦ — Dispatch ˒239 — SwiftFlux ˒212 — FluxWithRxSwiftSample ˒109 — FluxxKit ˒33

Slide 16

Slide 16 text

ݕ౼݁Ռ — ReSwift͕Ұ൪༗໊ — ؔ਺΍ΫϩʔδϟʔΛத৺ʹઃܭ͞Ε͓ͯΓɺ࣮૷͕ͱͯ ΋៉ྷ — ϦϦʔεස౓΋΄ͲΑ͘ɺISSUE΋׆ൃ

Slide 17

Slide 17 text

ɹ ɹ ReSwi!ͱ͸ ReSwi!ͷ࣮૷ͱར༻ྫΛݟͯΈΔ

Slide 18

Slide 18 text

State Action Reducer Dispatch ActionCreator

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

State protocol StateType { }

Slide 21

Slide 21 text

State / e.g. struct AppState: ReSwift.StateType { var timelineState = TimelineState() var userProfileState = UserProfileState() } struct TimelineState: ReSwift.StateType { var tweets: [Tweet] var response: [Tweet] }

Slide 22

Slide 22 text

State — Structͷ໦ߏ଄ — ֤Struct͸ReSwi!.StateTypeϓϩτίϧʹ४ڌ͢Δ — ׳ྫతʹҰ൪্૚ͷঢ়ଶΛද͢StructΛAppStateͱ͢Δ

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

Action protocol Action { }

Slide 25

Slide 25 text

Action / e.g. extension TimelineState { enum Action: ReSwift.Action { case requestSuccess(response: [Tweet]) case requestState(fetching: Bool) case requestError(error: Error) } }

Slide 26

Slide 26 text

Action — ReSwi!.Actionϓϩτίϧʹ४ڌ͍ͯ͠Ε͹ɺStructͰ ΋ɺEnumͰ΋Α͍ — ॲཧΛ༗͠ͳ͍ͨͩͷσʔλ — ReducerͰͲ͏͍͏ॲཧΛ͍͔ͨ͠ͷछྨͱͦͷΠϯϓο τσʔλʹͳΔ

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

Reducer typealias Reducer = (action: Action, state: ReducerStateType?) -> ReducerStateType

Slide 29

Slide 29 text

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 } }

Slide 30

Slide 30 text

Reducer — ReduxͰ͸ঢ়ଶมߋͰ͖Δͷ͸Reducer͚ͩͱ͍͏੍໿ — Reducer͸ (state, action) => state Λຬͨ͢ঢ়ଶΛ࣋ͨ ͳ͍;ͭ͏ͷؔ਺ʢ७ਮؔ਺ʣͰͳ͚Ε͹ͳΒͳ͍

Slide 31

Slide 31 text

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( reducer: appReduce, state: nil, middleware: [])

Slide 32

Slide 32 text

Reducer / Initialization — ׳ྫతʹҰ൪্૚ͷঢ়ଶΛද͢StructΛappReduceͱ͢Δ — appReduceΛى఺ʹɺԼҐͷReducerʹActionΛϒϩʔυ Ωϟετ͢Δ

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

Store open class Store: ReSwift.StoreType { var state: State! { get } private var reducer: Reducer open func dispatch(_ action: Action) { ... } open func subscribe(_ subscriber: S) { ... } open func unsubscribe(_ subscriber: AnyStoreSubscriber) { ... } ... }

Slide 35

Slide 35 text

Store — StateͱReducerΛอ࣋͢Δγϯάϧτϯ — ActionͷσΟεύονϝιουΛ༗͢Δ — StateͷαϒεΫϥΠϒϝιουΛ༗͢Δ

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

Dispatch / e.g. let action = TimelineState.Action.requestState(fetching: true) appStore.dispatch(action)

Slide 38

Slide 38 text

Dispatch — ViewଆͰActionͷΠϯελϯεΛ࡞੒͠ɺStoreͷ DispatchϝιουΛίʔϧ͢Δ

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

ActionCreator typealias ActionCreator = (state: ReSwift.State, store: ReSwift.StoreType) -> ReSwift.Action?

Slide 41

Slide 41 text

ActionCreator — DispatchՄೳͳؔ਺ͰActionΛฦ͢ — ActionCreatorؔ਺಺Ͱ͸StateʹΞΫηεՄೳͰɺState ΛՃ޻ͯ͠ActionΛ࡞੒͢Δ༻్ʹར༻Ͱ͖Δ — ActionCreatorؔ਺಺Ͱ͸StoreʹΞΫηεՄೳͰɺ DispatchϝιουΛίʔϧ͢Δ͜ͱ΋Ͱ͖Δ

Slide 42

Slide 42 text

ɹ ɹ ReSwi! Rx׆༻ฤ ෭࡞༻ʢඇಉظ௨৴ʣ/ ViewDataBinding

Slide 43

Slide 43 text

෭࡞༻ʢඇಉظ௨৴ʣ/ Asynchronous Operations ReSwi!ͷREADMEʹΑΔඇಉظ௨৴ͷྫ ActionCreator಺Ͱ෭࡞༻ʢඇಉظ௨৴ʣΛߦ͍ͬͯΔ func fetchGitHubRepositories(state: State, store: Store) -> 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 }

Slide 44

Slide 44 text

෭࡞༻ʢඇಉظ௨৴ʣ/ Asynchronous Operations — ActionCreatorؔ਺͕७ਮؔ਺Ͱ͸ͳ͘ͳΔ — ؔ਺಺ʹ෭࡞༻ʢඇಉظ௨৴ʣ͕͋Δͱςετ͕͠ʹ͍͘ — ඒ͘͠ͳ͍(ݸਓͷײ૝Ͱ͢) — ͱ͸͍͑ɺReducer͸७ਮؔ਺Ͱ෭࡞༻Λڐ༰͠ͳ͍ͷ ͰɺReducerʹ΋هड़Ͱ͖ͳ͍

Slide 45

Slide 45 text

ɹ ɹ Redux(JS)Ͱ͸ MiddlewareͰ෭࡞༻(ඇಉظॲཧ)Λѻ͏

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

Middleware public typealias DispatchFunction = (Action) -> Void public typealias Middleware = ( @escaping DispatchFunction, @escaping () -> State? ) -> (@escaping DispatchFunction) -> DispatchFunction

Slide 48

Slide 48 text

Middleware / e.g. let loggingMiddleware: ReSwift.Middleware = { dispatch, getState in return { next in return { action in logger.info("! [Action] \(action)") return next(action) } } var appStore = Store( reducer: appReduce, state: nil, middleware: [loggingMiddleware] )

Slide 49

Slide 49 text

ɹ ɹ Redux(JS)ͷ ඇಉظ༻Middlewareͨͪ

Slide 50

Slide 50 text

refs. Redux Middleware ɹ redux-thunk redux-sage redux-promise

Slide 51

Slide 51 text

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;

Slide 52

Slide 52 text

ɹ ɹ ;Ή;Ή!

Slide 53

Slide 53 text

ɹ ɹ RxSwi! ׆༻ͯ͠ඇಉظॲཧΛMiddlewareʹԡ͠ࠐΉ

Slide 54

Slide 54 text

rxThunkMiddleware struct SingleAction: ReSwift.Action { public let single: Single public let disposeBag: DisposeBag } let rxThunkMiddleware: ReSwift.Middleware = { 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) } } } }

Slide 55

Slide 55 text

rxThunkMiddleware / e.g. func requstAsyncCreator(maxID: String) -> Store.ActionCreator { return { (state: AppState, store: Store) 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.just(action) } return SingleAction(single: s, disposeBag: state.timelineState.requestDisposeBag) } }

Slide 56

Slide 56 text

ɹ ɹ Testability

Slide 57

Slide 57 text

ɹ ɹ Reduxͷੈք͸Πϛϡʔλϒϧ ७ਮؔ਺ɺ෭࡞༻ͷΈͰߏங͞ΕΔ

Slide 58

Slide 58 text

— Πϛϡʔλϒϧ͸ෆม — ActionʢΠϯϓοτύϥϝʔλʔʣɺStateʢσʔλʣ — ७ਮؔ਺͸ςετ͕༰қ — ೚ҙͷΠϯϓοτʹରͯ͠Ξ΢τϓοτ͕Ұҙʹܾ·Δ — ActionCreatorͱReducer — ෭࡞༻͸ςετ͕ෳࡶ — Middlewareͷ෦෼͸ςετ࣌ʹελϒʹࠩ͠ସ͑

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

ɹ ɹ DIෆཁ! Dependency Injection

Slide 61

Slide 61 text

ɹ ɹ ViewDataBinding

Slide 62

Slide 62 text

ɹ ɹ ReSwi!#subscribe

Slide 63

Slide 63 text

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() } }

Slide 64

Slide 64 text

ɹ ɹ RxSwi! ReSwi!.StoreΛRxSwi!.Variableʹม׵

Slide 65

Slide 65 text

let rxReduxStore = RxReduxStore(store: appStore) public class RxReduxStore: StoreSubscriber where AppStateType: StateType { public lazy var stateObservable: Observable = { return self.stateVariable.asObservable().observeOn(MainScheduler.instance) .shareReplayLatestWhileConnected() }() public var state: AppStateType { return stateVariable.value } private let stateVariable: Variable private let store: Store public init(store: Store) { 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) -> Action?) { store.dispatch(actionCreatorProvider) } }

Slide 66

Slide 66 text

class TimeLineViewController: UIViewController { fileprivate let disposeBag = DisposeBag() fileprivate let rxReduxStore: RxReduxStore init(_ rxReduxStore: RxReduxStore) { 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) } }

Slide 67

Slide 67 text

ɹ ɹ Viewͷࠩ෼ߋ৽

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

ɹ ɹ ReactͰ͸

Slide 70

Slide 70 text

ɹ ɹ Virtual DOM

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

ɹ ɹ iOSͰ͸

Slide 73

Slide 73 text

ɹ ɹ IGListKit1 1 https://github.com/Instagram/IGListKit

Slide 74

Slide 74 text

ɹ ɹ σʔλࠩ෼ΞϧΰϦζϜʹΑΔߴ଎ࠩ෼ߋ৽

Slide 75

Slide 75 text

ɹ ɹ ίϯϙʔωϯτࢦ޲Ͱଟ༷ͳViewΛѻ͑Δ

Slide 76

Slide 76 text

No content

Slide 77

Slide 77 text

— UICollectionViewΛϕʔεʹͰ͖͍ͯΔ — σʔλ͸͍ΖΜͳܕͷཁૉΛؚΉ̍࣍ݩ഑ྻ — σʔλͷཁૉͷܕͰίϯϙʔωϯτΛ෼ذ͢Δ — σʔλͷཁૉ͸ൺֱతՄೳͳͨΊʹIGListKitͷϓϩτίϧ ʹ४ڌ͢Δ(EquitableΈ͍ͨͳ΋ͷ) — ObjCͳͱ͜Ζ͕൵͍͠! — ϓϩτίϧ४ڌͷͨΊʹNSObjectΛܧঝ͕ඞཁ

Slide 78

Slide 78 text

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: return TweetsSectionController(o) case let o as DiffableWrap<[RecommendUser]>: return RecommendUsersSectionController(o) } } } }

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

ɹ ɹ One more thing !

Slide 82

Slide 82 text

ɹ ɹ Redux͸Debug͕͠΍͍͢

Slide 83

Slide 83 text

ɹ ɹ ReduxDevToolsͰঢ়ଶͷϞχλϦϯά

Slide 84

Slide 84 text

ɹ ɹ DEMO

Slide 85

Slide 85 text

Conclusion ɹ ɹ ReduxΞʔΩςΫνϟྑ͍Α!