Slide 1

Slide 1 text

ReduxͰ10ຊऑͷΞϓϦΛ ࡞ͬͨฐࣾͷφϨοδɺ࣌ؒͷ ڐ͢ݶΓ࿩͢Α kuroruri

Slide 2

Slide 2 text

ࣗݾ঺հɿ͘ΖΔΓ - teamLab.Inc - εϚʔτϑΥϯνʔϜɿΞʔΩςΫνϟ୲౰ - νʔϜ಺Ͱ։ൃ͞ΕΔ༷ʑͳΞϓϦͷΞʔΩςΫνϟબఆ / ։ ൃޮ཰Խ / ࣮૷૬ஊ໾ͳͲ͕ओͳ࢓ࣄ - Twitter - kuroruri - GitHub - kuroruriShield

Slide 3

Slide 3 text

࿩͢͜ͱ - ಘΒΕͨϝϦοτ / ݟ͖͑ͯͨ՝୊ - Reduxʹ͓͚ΔViewͷߋ৽ʹ͍ͭͯͷ໰୊ - UnitTestͷ͠΍͢͞ɺ࢓ํ - Store͸ΞϓϦશମͰSingletonͰ͋Δ΂͖͔

Slide 4

Slide 4 text

࿩͞ͳ͍ / ࿩ͤͳ͍͜ͱ - Redux͍ͭͯͷৄ͍͠ղઆ - ࣮ࡍͷࡉ͔͍࣮૷಺༰

Slide 5

Slide 5 text

Redux ͓͞Β͍ͱ࢖ͬͯΔϥΠϒϥϦͷ࿩

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

࢖͏ϥΠϒϥϦ - ReSwift : https://github.com/ReSwift/ReSwift - SwiftͰͷRedux࣮૷ - RxSwift : https://github.com/ReactiveX/RxSwift - ओʹඇಉظॲཧपΓ - ͜ͷޙͷ࿩ʹͪΐͬͱग़ͯ͘Δ

Slide 8

Slide 8 text

Reduxʹ͓͚ΔViewͷߋ৽ʹ ͍ͭͯͷ໰୊

Slide 9

Slide 9 text

ྫɿ঎඼Ұཡ - ػೳཁ݅ - ঎඼ͷҰཡ͕ϦετܗࣜͰݟΕΔ - ঎඼Ұཡ͸APIͰऔಘ͢Δ - ௥ՃϩʔσΟϯάɺϓϧϦϑϨογϡ͕͋Δ - ঎඼ηϧʹ͸͓ؾʹೖΓ͔Ͳ͏͔ͷΞΠίϯද͕ࣔ͋Δ - ͓ؾʹೖΓΞΠίϯΛԡ͢ͱ͓ؾʹೖΓঢ়ଶߋ৽APIΛݺͿ - ͓ؾʹೖΓঢ়ଶͷߋ৽ʹ੒ޭ͢Δͱτʔετදࣔ

Slide 10

Slide 10 text

ྫɿ঎඼Ұཡ - State / Action struct ProductListState: StateType { /// ঎඼ͷҰཡ var list: [Product] = [] /// ϩʔσΟϯάঢ়ଶ var loadingType = LoadingType.none /// ͓ؾʹೖΓߋ৽ var isUpdatingFavoriteSucceeded: Bool? } extension ProductListState { struct LoadingStart: Action { let loadingType: LoadingType } struct LoadingSuccess: Action { let loadingType: LoadingType let response: [Product] } struct UpdateFavoriteStart: Action {} struct UpdateFavoriteSuccess: Action {} }

Slide 11

Slide 11 text

ྫɿ঎඼Ұཡ - State / Action extension ProductListState { static func reduce(action: Action, state: ProductListState?) -> ProductListState { var nextState = state ?? ProductListState() switch action { case let action as ProductListState.LoadingStart: nextState.loadingType = action.loadingType case let action as ProductListState.LoadingSuccess: if action.loadingType == .add { nextState.list += action.response } else { nextState.list = action.response } case is UpdateFavoriteStart: nextState.isUpdatingFavoriteSucceeded = nil case is UpdateFavoriteSuccess: nextState.isUpdatingFavoriteSucceeded = true default: break } return nextState } }

Slide 12

Slide 12 text

ྫɿ঎඼Ұཡ - ViewController extension ProductListViewController: StoreSubscriber { func newState(state: ProductListState) { // ϩʔσΟϯάදࣔͷઃఆɺॳճͷΈΞχϝʔγϣϯͰޙ͸ඪ४UI·͔ͤ loadingView.needAnimation = state.loadingType == .initialize if state.loadingType == .none { refreshControl.endRefreshing() indicator.stopAnimating() } // σʔλ൓ө tableView.reloadData() // ͓ؾʹೖΓߋ৽Ͱ͖ͨΒτʔετ if state.isUpdatingFavoriteSucceeded ?? false { showFavoriteUpdatedToast() } } }

Slide 13

Slide 13 text

ྫɿ঎඼Ұཡ - ໰୊఺ - ௨৴Λ։͚࢝ͨͩ͠ͳͷʹtableView͕ෆཁʹreload͞ΕΔ - ͓ؾʹೖΓߋ৽ͨ͠௚ޙʹ͓ؾʹೖΓߋ৽Ҏ֎ͷߦಈΛ͢Δͱ ͓ؾʹೖΓߋ৽ͷτʔετ͕ग़Δ

Slide 14

Slide 14 text

ྫɿ঎඼Ұཡ - ** Flag / Actionͷ௥Ճ struct ProductListState: StateType { // --- ུ --- /// Ϧϩʔυ͕ඞཁ var isNeedReloadTableView = false } extension ProductListState { struct ResetNeedUpdateFlag: Action {} } extension ProductListState { static func reduce(action: Action, state: ProductListState?) -> ProductListState { // --- ུ --- case is ResetNeedUpdateFlag: nextState.isNeedReloadTableView = false nextState.isUpdatingFavoriteSucceeded = nil // --- ུ --- return nextState } }

Slide 15

Slide 15 text

ྫɿ঎඼Ұཡ - ** Actionͷdispatch extension ProductListViewController: StoreSubscriber { func newState(state: ProductListState) { // --- ུ --- var endAction: Action? // σʔλ൓ө if state.isNeedReloadTableView { tableView.reloadData() endAction = ProductListState.ResetNeedUpdateFlag() } // ͓ؾʹೖΓߋ৽Ͱ͖ͨΒτʔετ if state.isUpdatingFavoriteSucceeded ?? false { endAction = ProductListState.ResetNeedUpdateFlag() showFavoriteUpdatedToast() } if let endAction = endAction { store.dispatch(endAction) } } }

Slide 16

Slide 16 text

Viewͷࠩ෼ߋ৽໰୊ 1. ঢ়ଶ͕ߋ৽͞ΕΔͱViewͷߋ৽ʹඞཁͳ৘ใ͚ͩͰͳ͘શͯ ͷ৘ใ͕ৗʹྲྀΕͯ͘Δ 2. ͢Δͱߋ৽ͷτϦΨʔflagΛStateʹ࣋ͨͳͯ͘͸ͳΒͳ͍ 3. ݁ՌɺϏδωεϩδοΫͰ͋ΔState / Reducer / Action͕ݟ ͨ໨ͷ໰୊ʹ৵৯͞ΕΔ

Slide 17

Slide 17 text

ղܾ๏ - Ұ౓ʹྲྀΕͯ͘Δશ৘ใΛViewʹඞཁͳύϥϝʔλ෦෼͚ͩʹ Ճ޻͍ͨ͠ - มߋ͕ͳ͚Ε͹ແࢹ͍ͨ͠

Slide 18

Slide 18 text

Rx

Slide 19

Slide 19 text

RxʹΑΔRedux ( ReSwift ) ͷ֦ு - ࣮૷ࣗମ͸Լهͱಉ͡ܗͷ΋ͷΛνʔϜ಺Ͱ࢖༻͍ͯ͠Δ - ReSwiftͷεςʔτΛRxSwiftΛ࢖ͬͯ؂ࢹ͢Δ - Redux+RxΛ׆༻ͨ͠iOSΞϓϦΞʔΩςΫνϟ

Slide 20

Slide 20 text

Rx֦ுͨ͠৔߹ͷViewߋ৽ॲཧ func bind() { store.stateObservable.map { $0.list } .distinctUntilChanged { $0 == $1 } .subscribe(onNext: { [unowned self] _ in self.tableView.reloadData() }) .disposed(by: disposeBag) store.stateObservable.map { $0.isUpdatingFavoriteSucceeded } .distinctUntilChanged() .filter { $0 != nil } .map { $0! } .subscribe(onNext: { [unowned self] _ in self.showFavoriteUpdatedToast() }) .disposed(by: disposeBag) }

Slide 21

Slide 21 text

ReduxΛ࢖͏ͳΒRx΋ඞਢ

Slide 22

Slide 22 text

UnitTest

Slide 23

Slide 23 text

UnitTest͕ॻ͖ʹ͍͘ίʔυͷಛ௃ - ೖग़ྗ͕ෆ໌ྎ - ෭࡞༻͕͋Δ - ڍಈ͕֎෦ґଘ͍ͯ͠Δ

Slide 24

Slide 24 text

ReduxʢReSwift) 3FEVDFS "DUJPO$SFBUPS .JEEMFXBSF ೖग़ྗ ೖྗɿ4UBUF"DUJPO ग़ྗɿ4UBUF ೖྗɿ4UBUF"DUJPO ग़ྗɿ"DUJPO ೖྗɿ$BMMCBDL "DUJPO4UBUF ग़ྗɿ$BMMCBDL ෭࡞༻ ແ͠ ࣮૷ʹґଘ ࣮૷ʹґଘ ֎෦ґଘ ແ͠ ࣮૷ʹґଘ ࣮૷ʹґଘ

Slide 25

Slide 25 text

Reduxʹ͓͚ΔUnitTest - Reducer͸ؒҧ͍ͳ͘ΧόϨοδ100%ʹͰ͖Δ - 1(state)ʹର͠ +1(Action)ͨ͠Β2ʹͳΔ͔Ͱ͔͠ͳ͍ - ςετॳ৺ऀͰ΋ؒҧ͍ͳ͘ॻ͚Δ - ActionCreator / Middleware͸ઃܭΛߟ͑Δඞཁ͕͋Δ

Slide 26

Slide 26 text

ActionCreator / Middlewareͷ੹೚෼ׂ - (Async) ActionCreator - ෭࡞༻Λ࣋ͨͤͳ͍ - ୠ͠ΞϓϦ֎΁ͷ෭࡞༻͸আ͘ʢAPI௨৴ͳͲʣ - ग़ྗ͸ೖྗٴͼ֎෦ґଘʹґΔ - Middleware - ෭࡞༻͸͜͜Ͱߦ͏ʢRealm / UserDefault / KeyChain)

Slide 27

Slide 27 text

(Async) Action CreatorͷTestableԽ - ֎෦ґଘΛDIͰ͖ΔΑ͏ʹ͢Δ - ৗʹಉ݁͡ՌΛฦ͢MockΛDI͢Ε͹ɺग़ྗ͸ೖྗʹͷΈґଘ ͢Δ - Action͕dispatch͞Ε͔ͨͲ͏͔͸MiddlewareͰݕग़Ͱ͖Δ

Slide 28

Slide 28 text

ྫɿAsyncActionCreaotr with API public class RequestSampleActionCreator { private let request: SampleRequestable public init(request: SampleRequestable) { self.request = request } public func getLatestVersion(disposeBag: DisposeBag) -> Store.AsyncActionCreator { return { [weak self] (state, store, callback) in callback { _, _ in RequestSampleRequestStartAction() } self?.request.get(parameters: RequestSampleParameter()) .subscribe( onSuccess: { let action = RequestSampleResultAction(response: $0) callback { _, _ in action } }, onError: { let action = RequestSampleErrorAction(error: $0) callback { _, _ in action } }) .disposed(by: disposeBag) } } }

Slide 29

Slide 29 text

ActionCreatorςετ༻Middleware class ActionTestMiddleware { class func checkAsyncActionType(expectation: XCTestExpectation, actionDescription: String) -> Middleware { return { dispatch, getState in return { next in return { action in if String(describing: type(of: action)) == actionDescription { expectation.fulfill() } return next(action) } } } } }

Slide 30

Slide 30 text

XCTest : ActionCreator class RequestSampleReduxTest: XCTestCase { /// ActionCreatorͷςετɺAPI͕੒ޭͨ͠ͱ͖ʹਖ਼͍͠Action͕dispatch͞ΕΔ͔ͷςετ func testAsyncSuccessExample() { // testing ActionCreator, onSuccess asyncSuccessExpectation = expectation(description: "onSuccess") let middleware = ActionTestMiddleware.checkAsyncActionType( expectation: asyncSuccessExpectation, actionDescription: String(describing: RequestSampleResultAction.self)) store = Store(reducer: RequestSampleReducer.handleAction, state: nil, middleware: [middleware]) actionCreator = RequestSampleActionCreator(request: MockSampleRequest()) store.dispatch(actionCreator.getLatestVersion(disposeBag: disposeBag)) waitForExpectations(timeout: 5, handler: nil) } }

Slide 31

Slide 31 text

Middleware - ΍Γ͍ͨ͜ͱʹґ࣮ͬͯ૷͕มΘΔͷͰߴ౓ͷॊೈੑΛҡ࣋͠ ͭͭྟػԠมʹ

Slide 32

Slide 32 text

͔͜͜Βઌͷςετ - ਓؒʹΑΔςετͰൃݟ͞ΕͨόάͷओͳݪҼ - UserInputʹର͠ظ଴͞ΕͨAction͕dispatch͞Ε͍ͯͳ͍ - ظ଴͞Εͨॱ൪ͰAction͕dispatch͞Ε͍ͯͳ͍ - ಈ࡞γφϦΦΛҙࣝͨ͠Reducerͷςετίʔυ - ࠷ऴతʹ͸UITest

Slide 33

Slide 33 text

Redux͸ςετ͕ॻ͖΍͍͢ߏ੒ʹ͠΍͍͢ ςετίʔυΛॻ͘࠷ॳͷҰาΛ౿Έग़͠΍͍͢

Slide 34

Slide 34 text

Store͸ΞϓϦશମͰ SingletonͰ͋Δ΂͖͔

Slide 35

Slide 35 text

Singletonͳͷ͕ReduxͰ͸ʁ

Slide 36

Slide 36 text

ReSwift : αϯϓϧίʔυ let mainStore = Store( reducer: counterReducer, state: nil ) @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { [...] } To maintain our state and delegate the actions to the reducers, we need a store. Let's call it mainStore and define it as a global constant, for example in the app delegate file:

Slide 37

Slide 37 text

AppStore

Slide 38

Slide 38 text

AppStore : ViewController - TodayViewController - GameViewController - AppViewController - UpdateViewController - SearchViewController - AppDetailViewController

Slide 39

Slide 39 text

AppStore : AppState struct ApplicationState: StateType { typealias AppIdentifier = String var todayViewState: TodayViewState? // or todayViewState = TodayViewState() var gameViewState: GameViewState? var appViewState: AppViewState? var updateViewState: UpdateViewState? var searchViewState: SearchViewState? var appDetailViewState: [AppIdentifier: AppDetailViewState] = [:] }

Slide 40

Slide 40 text

Web : 1 Page, 1 Store 4UPSF 4UPSF 4UPSF

Slide 41

Slide 41 text

େ͖͗͢ΔStateʹର͢Δฐ֐ - State͕ංେԽ͢ΔͱOptimizerΛ༗ޮԽ͠ͳ͍ͱ࣮ߦதʹϝϞϦ ؔ࿈ͰΞϓϦ͕Ϋϥογϡ͢ΔՄೳੑ͕͋Δ - ຊ౰ʹStateͷංେԽ͕ݪҼ͔͸ݱࡏௐࠪத - ࢠStateΛඇOptionalܕʹͯͨ͠ͷ͕ݪҼ͔΋ʁ
 ( e.g. let childState = ChildState() ) - ڊେͳstructͷίϐʔͰcrash͢Δswiftͷόά͕͋ͬͨΒ͍͠ͷ ͰͦΕ͔΋͠Εͳ͍ɺमਖ਼͸ϚʔδࡁΈͳͷͰSwift4.2Ͱ௚Δ ͔΋

Slide 42

Slide 42 text

ViewController : Store = 1 : 1 Ͱ΋ྑ͍ ※ঢ়ଶͷҾ͖ܧ͕͗ଟ͍ը໘ભҠͳΒػೳ୯ҐͰStoreΛ۠੾Δͷ΋ΞϦ
 ( e.g. ձһొ࿥ϑϩʔͳͲ ) 4UPSF

Slide 43

Slide 43 text

࿈ଓ͠ͳ͍ը໘ؒͰڞ༗͢Δ৘ใ͸ʁ

Slide 44

Slide 44 text

MiddlewareʹΑΔ఻೻ - Ұ୴MiddlewareͰؤுͬͯΈΔ͜ͱʹ - https://gist.github.com/KuroruriShield/ bb9eba771fe2914569cc80ea5733fa08 - ΞϓϦ಺Ͱѻ͏શ෦ͷ৘ใΛશը໘Ͱڞ༗͢Δඞཁ͸ͳ͍ͱࢥ ͏ͷͰɺͲ͏ͯ͠΋ΞϓϦ಺Ͱඞͣ1ιʔεʹ͠ͳ͚Ε͹ͳΒͳ ͍৘ใ͕͋ΔͳΒͦΕઐ༻ͷStoreΛ۠੾ΔFluxϥΠΫͳ࣮૷ʹ ྑ͍͔΋

Slide 45

Slide 45 text

Flux / ReactorKitͰΑ͍ͷͰ͸ʁ

Slide 46

Slide 46 text

Flux / Redux / ReactorKit - ͲΕΛ࢖ͬͯ΋ྑ͍ͱࢥ͏ɺ։ൃऀͷ޷ΈͰྑ͍ - ςετͷ͠΍͢͞ / ঢ়ଶมߋͷ1ํ޲ੑ͸มΘΒͳ͍. - ΞʔΩςΫνϟͷن໛΋ಉ͘͡Β͍ - ݸਓతʹ͸Reduxͷߏ੒͕Ұ൪γϯϓϧͩͱࢥ͏ͷͰɺ
 ΋͏গ͠ଓ͚Δ༧ఆ - ReactorKit͸ࢼ͍͕ͨ͠ެࣜͷcarthageରԠ͕drop͞Εͨͷ ͰݟૹΓதɺFramework෼͚ͯΔߏ੒ͰCocoaPodsϥΠϒϥ ϦΛෳ਺૚ʹΠϯετʔϧ͢Δͷʹखؒऔ͍ͬͯΔ

Slide 47

Slide 47 text

·ͱΊ

Slide 48

Slide 48 text

ReduxΛશࣾతʹಋೖͯ͠ݟ͑ͨࣄ - Viewͷࠩ෼ߋ৽؅ཧͷͨΊReactive Extension͕΄΅ඞਢ - UnitTest͸ॻ͖΍͍͢ɺॻͨ͘ΊͷԼ஍ΛνʔϜʹ࡞ΕΔ - Store͸ΞϓϦશମͰSingletonʹ߆Βͣ੹೚෼ׂΛ͢Δ

Slide 49

Slide 49 text

൪֎ࢿྉ
 ࠙਌ձٞ࿦ & ૝ఆ࣭໰

Slide 50

Slide 50 text

Store͸ΞϓϦશମͰSingletonͰ͋Δ΂͖͔ - Application Singletonͩͱશ෦ݟָ͑ͯͳΜ͚ͩͲ - ָͳͷ͸͔֬ͩ͠ɺͦͷ΄͏͕ྑ͍ύλʔϯ΋͋Δͷ͸Θ͔ Δɺن໛ͷখ͍͞ΞϓϦͱ͔ɺͰ΋ن໛͕େ͖͘ͳΔͳΒ༨ ܭʹείʔϓΛ۠੾ͬͯػೳؒͷґଘΛͳ͘͢ߏ੒ʹͨ͠΄͏ ͕ޙʑָʹͳΔͱࢥ͏ ࠙਌ձ

Slide 51

Slide 51 text

ApplicationSingletonStoreͷϝϦοτ / σϝϦοτ - ϝϦοτ - શ৘ใΛશը໘Ͱڞ༗Ͱ͖Δ - ࢖͍ͬͯͨը໘ΛҰճഁغͯ͠΋StateΛഁغ͖͠Βͳ͚Ε͹ ϝϞϦʹΩϟογϡ͕࢒ΔͷͰɺը໘࠶දࣔ࣌ʹطଘͷ৘ใ ͕ग़ͤΔͷUXతͳϝϦοτ͕͋Δ - σϝϦοτ - Stateͷ֊૚ߏ଄͕ը໘ͷભҠߏ଄ʹґଘ͢Δ - ඞཁʹԠͯ͡ॳظԽ / ഁغΛߟ͑ͳ͚Ε͹ͳΒͳ͍ ࠙਌ձ

Slide 52

Slide 52 text

ͦͷଞ - Reduxͷֶशίετ͸΍͸Γൺֱతߴ͔ͬͨʁ - ߴ͔ͬͨɺ͕ͩRedux / Fluxֶ͕शίετ͕ߴ͍ΞʔΩςΫ νϟͩͱ͸࠷ۙࢥ͑ͳ͘ͳ͖ͬͯͨɺ୯ʹͦΕ·Ͱ੹೚ͷ෼ׂ ΍֎෦ґଘΛ͠ͳ͍ɺಠཱੑɺϝϯςφϯεੑͷߴ͍ઃܭΛ͖ ͪΜͱߟ͑ͯίʔυ͕ॻ͚͓ͯΒͣɺReduxͰͦͷลΓΛҙࣝ ͤ͟ΔΛಘͳ͘ͳͬͨͷ͕ݪҼ͔΋͠Εͳ͍ - Ͱ΋State΍Action͸ͲͷΑ͏ʹఆٛ͢΂͖͔೰Ήύλʔϯ΋ ଟ͔ͬͨͷͰɺ͕ͦ͜ϘτϧωοΫͳͷ͔΋ ࠙਌ձ

Slide 53

Slide 53 text

Reduxʹ͓͚ΔViewͷߋ৽ʹ͍ͭͯͷ໰୊ - ݫີʹEquatableʹग़དྷͳ͍ܕͷϓϩύςΟ͸(Error?ͱ͔)Ͳ͏ ͢Δʁ - EquatableͰϢχʔΫͳσʔλͱ߹Θͤͯtupleʹͯ͠ɺ
 ͦͷϢχʔΫͳσʔλ͕ == ͔Ͱ൑அ͢Δํ๏Λࠓ͸औͬͯΔ
 let error: ( uuidString: String, error: Error)? ૝ఆ࣭໰

Slide 54

Slide 54 text

ͦͷଞ - ϓϩϙʔβϧͷSwaggerͰϏδωεϩδοΫࣗಈੜ੒͢Δ࿩͸ Կॲ΁͍ͬͨ - Android ͱ iOSͰϏδωεϩδοΫڞ௨ͳ͜ͱ͸ଟ͍ͷͰɺ ReduxͷลΓΛࣗಈੜ੒Ͱ͖Ε͹ͱ͸ߟ͍͑ͯͨ - ࠓ೔ʹ޲͚ͯ݁Ռ·ͱΊ͕ͯͨࣗಈੜ੒ࣗମͷϓϥϯΛେ͖ ͘࿏ઢมߋ͢Δඞཁ͕͋ͬͨͷͰɺ͢Έ·ͤΜຊ೔͸ݟૹΒ ͍ͤͯͩ͘͞ - Ͱ΋ݪཧతʹ׬શෆՄೳͰ͸ͳ͍ͱࢥ͏ͷͰҾ͖ଓ͖ؤுΔ ૝ఆ࣭໰