I am building a new app, comparable with Instagram in that you can "explore" a list of photos, then view an individual photo and its comments.
I've structured the state like this, with a top-level AppState
container which holds ExploreState
, which in turn contains an IdentifiedArrayOf
of photos (among other things like filters, a loading
property, etc). Each photo can contain a list of comments.
// ---------
// App State
// ---------
struct AppState: Equatable {
var exploreState = ExploreState()
}
enum AppAction: Equatable {
case exploreAction(ExploreAction)
}
let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
Reducer { state, action, _ in
switch action {
case .exploreAction:
return .none
}
},
exploreReducer.pullback(
state: \.exploreState,
action: /AppAction.exploreAction,
environment: { $0 }
)
)
// -------------
// Explore State
// -------------
struct ExploreState: Equatable {
var photos: IdentifiedArrayOf<Photo> = .init()
var loading = false
var keyword: String?
var username: String?
var page = 1
}
enum ExploreAction: Equatable {
case loadPhotos
case loadPhotosResponse(Result<[Photo], ApiError>)
case photo(id: Int, action: PhotoAction)
}
let exploreReducer = Reducer<ExploreState, ExploreAction, AppEnvironment>.combine(
Reducer { state, action, environment in
switch action {
case .loadPhotos:
// kick off API effect
case .loadPhotosResponse(.success(let photos)):
state.photos = photos
return .none
case .photo:
return .none
}
},
photoReducer.forEach(
state: \.photos,
action: /ExploreAction.photo(id:action:),
environment: { $0 }
)
)
// -----------
// Photo State
// -----------
struct Photo: Codable, Identifiable, Hashable {
let id: Int
let user: User
var image: String
let thumbnail: String
var description: String?
var comments: [Comment]?
}
enum PhotoAction: Equatable {
case loadComments
case loadCommentsResponse(Result<[Comment], ApiError>)
}
let photoReducer = Reducer<Photo, PhotoAction, AppEnvironment> { state, action, environment in
switch action {
case .loadComments:
// kick off API effect
case .loadCommentsResponse(.success(let comments)):
state.comments = comments
return .none
}
}
It all works fine so far, I can create scoped stores and hand them off to coordinators / view models. Loading comments for a photo in the photoStore
does update all the way to the top level appStore
, everything is great.
let exploreStore = appStore.scope(state: { $0.exploreState }, action: { AppAction.exploreAction($0) })
let photoStore = exploreStore.scope(state: { $0.photos[id: id] }, action: { .photo(id: id, action: $0) })
Okay, so now finally my question :)
Users need to be able to login, once logged in they can upload photos, add their own comments to photos, etc. My thinking was to add some kind of AccountState
to the top level AppState
, with its own actions and reducer, which would be responsible for all account-related things (login, logout, change profile details, etc). But how would the photo details page, which just gets handed a ViewStore<Photo, PhotoAction>
to work with, know if a user is logged in? It seems silly to pass two stores to a viewmodel, right? Like I'd hand it both a ViewStore<Photo, PhotoAction>
and a ViewStore<AccountState, AccountAction>
?
I am not sure how best to architect this state, any insights would be greatly appreciated, thanks!