Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Knowledge obtained by making about 10 apps with Redux in iOS

Kuroruri
September 12, 2018

Knowledge obtained by making about 10 apps with Redux in iOS

俺コン2018
Reduxで10本弱のアプリを作った弊社のナレッジ、時間の許す限り話すよ

Kuroruri

September 12, 2018
Tweet

More Decks by Kuroruri

Other Decks in Programming

Transcript

  1. ࢖͏ϥΠϒϥϦ - ReSwift : https://github.com/ReSwift/ReSwift - SwiftͰͷRedux࣮૷ - RxSwift :

    https://github.com/ReactiveX/RxSwift - ओʹඇಉظॲཧपΓ - ͜ͷޙͷ࿩ʹͪΐͬͱग़ͯ͘Δ
  2. ྫɿ঎඼Ұཡ - ػೳཁ݅ - ঎඼ͷҰཡ͕ϦετܗࣜͰݟΕΔ - ঎඼Ұཡ͸APIͰऔಘ͢Δ - ௥ՃϩʔσΟϯάɺϓϧϦϑϨογϡ͕͋Δ -

    ঎඼ηϧʹ͸͓ؾʹೖΓ͔Ͳ͏͔ͷΞΠίϯද͕ࣔ͋Δ - ͓ؾʹೖΓΞΠίϯΛԡ͢ͱ͓ؾʹೖΓঢ়ଶߋ৽APIΛݺͿ - ͓ؾʹೖΓঢ়ଶͷߋ৽ʹ੒ޭ͢Δͱτʔετදࣔ
  3. ྫɿ঎඼Ұཡ - 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 {} }
  4. ྫɿ঎඼Ұཡ - 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 } }
  5. ྫɿ঎඼Ұཡ - 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() } } }
  6. ྫɿ঎඼Ұཡ - ** 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 } }
  7. ྫɿ঎඼Ұཡ - ** 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) } } }
  8. Rx

  9. 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) }
  10. ReduxʢReSwift) 3FEVDFS "DUJPO$SFBUPS .JEEMFXBSF ೖग़ྗ ೖྗɿ4UBUF"DUJPO ग़ྗɿ4UBUF ೖྗɿ4UBUF"DUJPO ग़ྗɿ"DUJPO ೖྗɿ$BMMCBDL

    "DUJPO4UBUF ग़ྗɿ$BMMCBDL ෭࡞༻ ແ͠ ࣮૷ʹґଘ ࣮૷ʹґଘ ֎෦ґଘ ແ͠ ࣮૷ʹґଘ ࣮૷ʹґଘ
  11. ActionCreator / Middlewareͷ੹೚෼ׂ - (Async) ActionCreator - ෭࡞༻Λ࣋ͨͤͳ͍ - ୠ͠ΞϓϦ֎΁ͷ෭࡞༻͸আ͘ʢAPI௨৴ͳͲʣ

    - ग़ྗ͸ೖྗٴͼ֎෦ґଘʹґΔ - Middleware - ෭࡞༻͸͜͜Ͱߦ͏ʢRealm / UserDefault / KeyChain)
  12. ྫɿ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) } } }
  13. 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) } } } } }
  14. 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) } }
  15. 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:
  16. AppStore : ViewController - TodayViewController - GameViewController - AppViewController -

    UpdateViewController - SearchViewController - AppDetailViewController
  17. 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] = [:] }
  18. Flux / Redux / ReactorKit - ͲΕΛ࢖ͬͯ΋ྑ͍ͱࢥ͏ɺ։ൃऀͷ޷ΈͰྑ͍ - ςετͷ͠΍͢͞ /

    ঢ়ଶมߋͷ1ํ޲ੑ͸มΘΒͳ͍. - ΞʔΩςΫνϟͷن໛΋ಉ͘͡Β͍ - ݸਓతʹ͸Reduxͷߏ੒͕Ұ൪γϯϓϧͩͱࢥ͏ͷͰɺ
 ΋͏গ͠ଓ͚Δ༧ఆ - ReactorKit͸ࢼ͍͕ͨ͠ެࣜͷcarthageରԠ͕drop͞Εͨͷ ͰݟૹΓதɺFramework෼͚ͯΔߏ੒ͰCocoaPodsϥΠϒϥ ϦΛෳ਺૚ʹΠϯετʔϧ͢Δͷʹखؒऔ͍ͬͯΔ
  19. ApplicationSingletonStoreͷϝϦοτ / σϝϦοτ - ϝϦοτ - શ৘ใΛશը໘Ͱڞ༗Ͱ͖Δ - ࢖͍ͬͯͨը໘ΛҰճഁغͯ͠΋StateΛഁغ͖͠Βͳ͚Ε͹ ϝϞϦʹΩϟογϡ͕࢒ΔͷͰɺը໘࠶දࣔ࣌ʹطଘͷ৘ใ

    ͕ग़ͤΔͷUXతͳϝϦοτ͕͋Δ - σϝϦοτ - Stateͷ֊૚ߏ଄͕ը໘ͷભҠߏ଄ʹґଘ͢Δ - ඞཁʹԠͯ͡ॳظԽ / ഁغΛߟ͑ͳ͚Ε͹ͳΒͳ͍ ࠙਌ձ
  20. ͦͷଞ - ϓϩϙʔβϧͷSwaggerͰϏδωεϩδοΫࣗಈੜ੒͢Δ࿩͸ Կॲ΁͍ͬͨ - Android ͱ iOSͰϏδωεϩδοΫڞ௨ͳ͜ͱ͸ଟ͍ͷͰɺ ReduxͷลΓΛࣗಈੜ੒Ͱ͖Ε͹ͱ͸ߟ͍͑ͯͨ -

    ࠓ೔ʹ޲͚ͯ݁Ռ·ͱΊ͕ͯͨࣗಈੜ੒ࣗମͷϓϥϯΛେ͖ ͘࿏ઢมߋ͢Δඞཁ͕͋ͬͨͷͰɺ͢Έ·ͤΜຊ೔͸ݟૹΒ ͍ͤͯͩ͘͞ - Ͱ΋ݪཧతʹ׬શෆՄೳͰ͸ͳ͍ͱࢥ͏ͷͰҾ͖ଓ͖ؤுΔ ૝ఆ࣭໰