Search code examples
swiftswift-composable-architecturetca

Generic state for TCA


I would like to make reusable solution to avoid boilerplate for lazy loading features

I have base state and base view model which describes common

@ObservableState
enum BaseState<Input, BaseContentState> {
  case progress(BaseProgressState<Input>)
  case content(BaseContentState)
  case error(BaseErrorState<Input>)

  init(input: Input) {
    self = .progress(BaseProgressState(input: input))
  }
}

@Reducer
struct BaseViewModel<Input, ContentInput, BaseContentState: BaseContentStateType, BaseContentAction> where ContentInput == BaseContentState.ContentInput {
  typealias State = BaseState<Input, BaseContentState>

  enum Action {
    case progress(BaseProgressAction<Input, ContentInput>)
    case content(BaseContentAction)
    case error(BaseErrorAction<Input>)
  }
}

BaseContentStateType allows to make content state buildable with some result of progress feature

protocol BaseContentStateType {
  associatedtype ContentInput
  init(contentInput: ContentInput)
}

Additional I have common implementation to show progress...

@ObservableState
struct BaseProgressState<Input> {
  let input: Input
}

enum BaseProgressAction<Input, ContentInput> {
  case load(Input)
  case loadSuccess(ContentInput)
  case loadFailure(Input, Error)
}

struct BaseProgressViewModel<Input, ContentInput>: Reducer {
  typealias State = BaseProgressState<Input>
  typealias Action = BaseProgressAction<Input, ContentInput>

  var body: some ReducerOf<Self> {
    EmptyReducer()
  }
}

struct BaseProgressView<Input, ContentInput>: View {
  let store: StoreOf<BaseProgressViewModel<Input, ContentInput>>

  var body: some View {
    ProgressView()
      .onAppear {
        store.send(.load(store.input))
      }
  }
}

... and error

@ObservableState
struct BaseErrorState<Input> {
  let input: Input
  let error: Error
}

enum BaseErrorAction<Input> {
  case retry(Input)
}

struct BaseErrorViewModel<Input>: Reducer {
  typealias State = BaseErrorState<Input>
  typealias Action = BaseErrorAction<Input>

  var body: some ReducerOf<Self> {
    EmptyReducer()
  }
}

struct BaseErrorView<Input>: View {
  let store: StoreOf<BaseErrorViewModel<Input>>

  var body: some View {
    Button("Retry") {
      store.send(.retry(store.input))
    }
  }
}

When I try to use this solution...

@Reducer
struct FeatureViewModel {
  typealias State = BaseViewModel<...>.State
  typealias Action = BaseViewModel<...>.Action

  var body: some ReducerOf<Self> {
    ...
  }
}

struct FeatureView: View {
  let store: StoreOf<FeatureViewModel>

  var body: some View {
    BaseView(store: store) { store in
      FeatureContentView(store: store)
    }
  }
}

... I face problems, like

Instance method 'ifLet(_:action:)' requires that 'FeatureViewModel.State' (aka 'BaseState<FeatureInput, FeatureContentViewModel.State>') conform to 'CaseReducerState'

And no matter how hard I try to get it to look right, I don't understand what I'm doing wrong. How to confirm CaseReducerState?


Solution

  • I forgot to make ViewModel for this case

    .ifLet(\.$myFeatureState, action: \.myFeatureAction) {
        MyFeatureViewModel()
    }