Multiple LoadableEnvironments?
Closed this issue · 3 comments
Thanks for this addition, one quick question.
I'm attempting to use this to manage persistence and requests to CoreData/CloudKit inside TCA.
As LoadableEnvironment supports loading one type (LoadedValue/load()), is the suggested approach to compose multiple LoadableEnvironments together per type? For example, fetching multiple Entities from CoreData (Users, Todos, Posts, etc).
Something like this:
import Foundation
import ComposableArchitecture
import TCALoadable
import Combine
struct AppEnvironment {
var nameLoader: NameLoader
var intLoader: IntLoader
}
struct NameLoader: LoadableEnvironment {
typealias LoadedValue = String
let failOnLoad: Bool
let mainQueue: AnySchedulerOf<DispatchQueue>
func load() -> Effect<String, Error> {
guard !failOnLoad else {
return Fail(error: NameError.failed)
.delay(for: .seconds(1), scheduler: mainQueue)
.eraseToEffect()
}
return Just("chad")
.setFailureType(to: Error.self)
// Simulate a network call or loading from disk.
.delay(for: .seconds(1), scheduler: mainQueue)
.eraseToEffect()
}
}
enum NameError: Error {
case failed
}
struct IntLoader: LoadableEnvironment {
typealias LoadedValue = Int
let failOnLoad: Bool
let mainQueue: AnySchedulerOf<DispatchQueue>
func load() -> Effect<Int, Error> {
guard !failOnLoad else {
return Fail(error: IntError.failed)
.delay(for: .seconds(1), scheduler: mainQueue)
.eraseToEffect()
}
return Just(100)
.setFailureType(to: Error.self)
// Simulate a network call or loading from disk.
.delay(for: .seconds(1), scheduler: mainQueue)
.eraseToEffect()
}
}
enum IntError: Error {
case failed
}
struct AppState: Equatable {
/// A score loaded from the environment.
var score: Loadable<Int> = .notRequested
var name: Loadable<String> = .notRequested
}
enum AppAction: Equatable {
case loadActions(LoadableActionsFor<Int>)
case loadNameAction(LoadableActionsFor<String>)
}
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
switch action {
case .loadActions(_):
return .none
case .loadNameAction(_):
return .none
}
}
.loadable(
state: \.score,
action: /AppAction.loadActions,
environment: { $0.intLoader }
)
.loadable(
state: \.name,
action: /AppAction.loadNameAction,
environment: { $0.nameLoader }
)
struct TestLoadableView: View {
let store: Store<AppState, AppAction>
var body: some View {
WithViewStore(store) { viewStore in
VStack {
LoadableView(
store: store.scope(
state: { $0.score },
action: { .loadActions($0) }
),
autoLoad: true
) { loadedScore in
Text("Congratulations your score is: \(loadedScore)")
}
notRequestedView: { ProgressView() }
isLoadingView: { _ in ProgressView() }
errorView: { error in
VStack {
Text("Oops, something went wrong!")
Text(error.localizedDescription)
.font(.callout)
Button(action: { viewStore.send(.loadActions(.load)) }) {
Text("Retry")
}
}
}
LoadableView(
store: store.scope(
state: { $0.name },
action: { .loadNameAction($0) }
),
autoLoad: true
) { loadedName in
Text("Congratulations your name is: \(loadedName)")
}
notRequestedView: { ProgressView() }
isLoadingView: { _ in ProgressView() }
errorView: { error in
VStack {
Text("Oops, something went wrong!")
Text(error.localizedDescription)
.font(.callout)
Button(action: { viewStore.send(.loadNameAction(.load)) }) {
Text("Retry")
}
}
}
}
}
}
}
struct Done_ComposableApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
TestLoadableView(store: Store(
initialState: AppState(),
reducer: appReducer,
environment: AppEnvironment(
nameLoader: NameLoader(failOnLoad: false, mainQueue: DispatchQueue.main.eraseToAnyScheduler()),
intLoader: IntLoader(failOnLoad: false, mainQueue: DispatchQueue.main.eraseToAnyScheduler())
)
))
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
I haven't used w/ core data yet. My use case for building was external API calls, where I get the entire type back. So loading would fail if the type wasn't decoded correctly. I would assume this approach would work on core data as well. So I guess it depends on how your entities are stored in core data.
struct UserScore: Equatable, Codable {
let name: String
let score: Int
}
struct UserScoreLoader: LoadableEnvironment {
typealias LoadableValue = UserScore
func load() -> Effect<UserScore, Error> {
Just(UserScore(name: "User", score: 100))
}
}
struct AppState: Equatable {
var userScore: Loadable<UserScore> = .notRequested
}
...
Thanks, I can imagine crafting a CoreDataResponse struct w/ the needed data types I'm trying to pull.
Regarding CoreData, on further investigation it appears CoreData (at least w/ cloudkit) doesn't play very nicely with the composable architecture as it mutates independently of state and keeping it in sync is quite a chore.