Redux+Rxを活用したiOSアプリアーキテクチャ

 Redux+Rxを活用したiOSアプリアーキテクチャ

俺コン Vol.1
2017/10/02@株式会社ディー・エヌ・エー

Yohei Suginami ( @susieyy )

Acbf3391de0494432a92221ffe89f34e?s=128

yohei sugigami

October 01, 2017
Tweet

Transcript

  1. 2.

    Profile — Yohei Sugigami — susieyy — Twitter / Qiita

    / Github — New App Development Specialization — Clients — Folio.inc — New app developer — Wantedly.inc — Technical advisor
  2. 9.
  3. 10.
  4. 17.
  5. 19.

    State / e.g. struct AppState: ReSwift.StateType { var timelineState =

    TimelineState() var userProfileState = UserProfileState() } struct TimelineState: ReSwift.StateType { var tweets: [Tweet] var response: [Tweet] }
  6. 21.
  7. 23.

    Action / e.g. extension TimelineState { enum Action: ReSwift.Action {

    case requestSuccess(response: [Tweet]) case requestState(fetching: Bool) case requestError(error: Error) } }
  8. 25.
  9. 27.

    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 } }
  10. 29.

    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: [])
  11. 31.
  12. 32.

    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) { ... } ... }
  13. 34.
  14. 37.
  15. 41.

    ෭࡞༻ʢඇಉظ௨৴ʣ/ 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 }
  16. 44.
  17. 45.

    Middleware public typealias DispatchFunction = (Action) -> Void public typealias

    Middleware<State> = ( @escaping DispatchFunction, @escaping () -> State? ) -> (@escaping DispatchFunction) -> DispatchFunction
  18. 46.

    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] )
  19. 49.

    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;
  20. 52.

    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) } } } }
  21. 53.

    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) } }
  22. 57.
  23. 61.

    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() } }
  24. 63.

    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) } }
  25. 64.

    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) } }
  26. 66.
  27. 69.
  28. 74.
  29. 76.

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