Share
Embed
Knowledge obtained by making about 10 apps with Redux in iOS
俺コン2018
Reduxで10本弱のアプリを作った弊社のナレッジ、時間の許す限り話すよ
Kuroruri
September 12, 2018
Transcript
54 slides
-
ࣗݾհɿ͘ΖΔΓ - teamLab.Inc - εϚʔτϑΥϯνʔϜɿΞʔΩςΫνϟ୲ - νʔϜͰ։ൃ͞ΕΔ༷ʑͳΞϓϦͷΞʔΩςΫνϟબఆ / ։ ൃޮԽ
/ ࣮૬ஊͳͲ͕ओͳࣄ - Twitter - kuroruri - GitHub - kuroruriShield -
͢͜ͱ - ಘΒΕͨϝϦοτ / ݟ͖͑ͯͨ՝ - Reduxʹ͓͚ΔViewͷߋ৽ʹ͍ͭͯͷ - UnitTestͷ͢͠͞ɺํ -
StoreΞϓϦશମͰSingletonͰ͋Δ͖͔ -
͏ϥΠϒϥϦ - ReSwift : https://github.com/ReSwift/ReSwift - SwiftͰͷRedux࣮ - RxSwift :
https://github.com/ReactiveX/RxSwift - ओʹඇಉظॲཧपΓ - ͜ͷޙͷʹͪΐͬͱग़ͯ͘Δ -
ྫɿҰཡ - ػೳཁ݅ - ͷҰཡ͕ϦετܗࣜͰݟΕΔ - ҰཡAPIͰऔಘ͢Δ - ՃϩʔσΟϯάɺϓϧϦϑϨογϡ͕͋Δ -
ηϧʹ͓ؾʹೖΓ͔Ͳ͏͔ͷΞΠίϯද͕ࣔ͋Δ - ͓ؾʹೖΓΞΠίϯΛԡ͢ͱ͓ؾʹೖΓঢ়ଶߋ৽APIΛݺͿ - ͓ؾʹೖΓঢ়ଶͷߋ৽ʹޭ͢Δͱτʔετදࣔ -
ྫɿҰཡ - 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 {} } -
ྫɿҰཡ - 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 } } -
ྫɿҰཡ - 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() } } } -
ྫɿҰཡ - ** 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 } } -
ྫɿҰཡ - ** 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) } } } -
RxʹΑΔRedux ( ReSwift ) ͷ֦ு - ࣮ࣗମԼهͱಉ͡ܗͷͷΛνʔϜͰ༻͍ͯ͠Δ - ReSwiftͷεςʔτΛRxSwiftΛͬͯࢹ͢Δ -
Redux+RxΛ׆༻ͨ͠iOSΞϓϦΞʔΩςΫνϟ -
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) } -
ReduxʢReSwift) 3FEVDFS "DUJPO$SFBUPS .JEEMFXBSF ೖग़ྗ ೖྗɿ4UBUF"DUJPO ग़ྗɿ4UBUF ೖྗɿ4UBUF"DUJPO ग़ྗɿ"DUJPO ೖྗɿ$BMMCBDL
"DUJPO4UBUF ग़ྗɿ$BMMCBDL ෭࡞༻ ແ͠ ࣮ʹґଘ ࣮ʹґଘ ֎෦ґଘ ແ͠ ࣮ʹґଘ ࣮ʹґଘ -
ActionCreator / Middlewareͷׂ - (Async) ActionCreator - ෭࡞༻Λ࣋ͨͤͳ͍ - ୠ͠ΞϓϦ֎ͷ෭࡞༻আ͘ʢAPI௨৴ͳͲʣ
- ग़ྗೖྗٴͼ֎෦ґଘʹґΔ - Middleware - ෭࡞༻͜͜Ͱߦ͏ʢRealm / UserDefault / KeyChain) -
ྫɿAsyncActionCreaotr with API public class RequestSampleActionCreator<T: StateType> { private let
request: SampleRequestable public init(request: SampleRequestable) { self.request = request } public func getLatestVersion(disposeBag: DisposeBag) -> Store<T>.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) } } } -
ActionCreatorςετ༻Middleware class ActionTestMiddleware { class func checkAsyncActionType(expectation: XCTestExpectation, actionDescription: String)
-> Middleware<StateType> { return { dispatch, getState in return { next in return { action in if String(describing: type(of: action)) == actionDescription { expectation.fulfill() } return next(action) } } } } } -
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<RequestSampleState>(reducer: RequestSampleReducer.handleAction, state: nil, middleware: [middleware]) actionCreator = RequestSampleActionCreator<RequestSampleState>(request: MockSampleRequest()) store.dispatch(actionCreator.getLatestVersion(disposeBag: disposeBag)) waitForExpectations(timeout: 5, handler: nil) } } -
ReSwift : αϯϓϧίʔυ let mainStore = Store<AppState>( 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: -
AppStore : ViewController - TodayViewController - GameViewController - AppViewController -
UpdateViewController - SearchViewController - AppDetailViewController -
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] = [:] } -
େ͖͗͢ΔStateʹର͢Δฐ - State͕ංେԽ͢ΔͱOptimizerΛ༗ޮԽ͠ͳ͍ͱ࣮ߦதʹϝϞϦ ؔ࿈ͰΞϓϦ͕Ϋϥογϡ͢ΔՄೳੑ͕͋Δ - ຊʹStateͷංେԽ͕ݪҼ͔ݱࡏௐࠪத - ࢠStateΛඇOptionalܕʹͯͨ͠ͷ͕ݪҼ͔ʁ ( e.g.
let childState = ChildState() ) - ڊେͳstructͷίϐʔͰcrash͢Δswiftͷόά͕͋ͬͨΒ͍͠ͷ ͰͦΕ͔͠Εͳ͍ɺमਖ਼ϚʔδࡁΈͳͷͰSwift4.2ͰΔ ͔ -
ViewController : Store = 1 : 1 Ͱྑ͍ ※ঢ়ଶͷҾ͖ܧ͕͗ଟ͍ը໘ભҠͳΒػೳ୯ҐͰStoreΛ۠ΔͷΞϦ (
e.g. ձһొϑϩʔͳͲ ) 4UPSF -
Flux / Redux / ReactorKit - ͲΕΛͬͯྑ͍ͱࢥ͏ɺ։ൃऀͷΈͰྑ͍ - ςετͷ͢͠͞ /
ঢ়ଶมߋͷ1ํੑมΘΒͳ͍. - ΞʔΩςΫνϟͷنಉ͘͡Β͍ - ݸਓతʹReduxͷߏ͕Ұ൪γϯϓϧͩͱࢥ͏ͷͰɺ ͏গ͠ଓ͚Δ༧ఆ - ReactorKitࢼ͍͕ͨ͠ެࣜͷcarthageରԠ͕drop͞Εͨͷ ͰݟૹΓதɺFramework͚ͯΔߏͰCocoaPodsϥΠϒϥ ϦΛෳʹΠϯετʔϧ͢Δͷʹखؒऔ͍ͬͯΔ -
ApplicationSingletonStoreͷϝϦοτ / σϝϦοτ - ϝϦοτ - શใΛશը໘Ͱڞ༗Ͱ͖Δ - ͍ͬͯͨը໘ΛҰճഁغͯ͠StateΛഁغ͖͠Βͳ͚Ε ϝϞϦʹΩϟογϡ͕ΔͷͰɺը໘࠶දࣔ࣌ʹطଘͷใ
͕ग़ͤΔͷUXతͳϝϦοτ͕͋Δ - σϝϦοτ - Stateͷ֊ߏ͕ը໘ͷભҠߏʹґଘ͢Δ - ඞཁʹԠͯ͡ॳظԽ / ഁغΛߟ͑ͳ͚ΕͳΒͳ͍ ࠙ձ -
ͦͷଞ - ReduxͷֶशίετΓൺֱతߴ͔ͬͨʁ - ߴ͔ͬͨɺ͕ͩRedux / Fluxֶ͕शίετ͕ߴ͍ΞʔΩςΫ νϟͩͱ࠷ۙࢥ͑ͳ͘ͳ͖ͬͯͨɺ୯ʹͦΕ·Ͱͷׂ ֎෦ґଘΛ͠ͳ͍ɺಠཱੑɺϝϯςφϯεੑͷߴ͍ઃܭΛ͖ ͪΜͱߟ͑ͯίʔυ͕ॻ͚͓ͯΒͣɺReduxͰͦͷลΓΛҙࣝ
ͤ͟ΔΛಘͳ͘ͳͬͨͷ͕ݪҼ͔͠Εͳ͍ - ͰStateActionͲͷΑ͏ʹఆ͖͔ٛ͢Ήύλʔϯ ଟ͔ͬͨͷͰɺ͕ͦ͜ϘτϧωοΫͳͷ͔ ࠙ձ -
Reduxʹ͓͚ΔViewͷߋ৽ʹ͍ͭͯͷ - ݫີʹEquatableʹग़དྷͳ͍ܕͷϓϩύςΟ(Error?ͱ͔)Ͳ͏ ͢Δʁ - EquatableͰϢχʔΫͳσʔλͱ߹Θͤͯtupleʹͯ͠ɺ ͦͷϢχʔΫͳσʔλ͕ == ͔Ͱஅ͢Δํ๏ΛࠓऔͬͯΔ let
error: ( uuidString: String, error: Error)? ఆ࣭ -
ͦͷଞ - ϓϩϙʔβϧͷSwaggerͰϏδωεϩδοΫࣗಈੜ͢Δ Կॲ͍ͬͨ - Android ͱ iOSͰϏδωεϩδοΫڞ௨ͳ͜ͱଟ͍ͷͰɺ ReduxͷลΓΛࣗಈੜͰ͖Εͱߟ͍͑ͯͨ -
ࠓʹ͚ͯ݁Ռ·ͱΊ͕ͯͨࣗಈੜࣗମͷϓϥϯΛେ͖ ͘࿏ઢมߋ͢Δඞཁ͕͋ͬͨͷͰɺ͢Έ·ͤΜຊݟૹΒ ͍ͤͯͩ͘͞ - ͰݪཧతʹશෆՄೳͰͳ͍ͱࢥ͏ͷͰҾ͖ଓ͖ؤுΔ ఆ࣭