eivindml
(Eivind Lindbråten)
1
Hi,
I'm trying to learn navigation using TCA, and want to create a macOS app with a sidebar. This is what I want to achieve:
Except with the text replaced with ProjectView() with the corresponding Blob Jr project.
NavigationView is deprecated and Apple recommends using NavigationSplitView for this it looks like.
Here's the code I have so far:
import SwiftUI
import ComposableArchitecture
struct Project: Equatable, Identifiable {
let id: UUID
var name: String
}
struct ProjectsFeature: Reducer {
struct State: Equatable {
var projects: IdentifiedArrayOf<Project> = []
var path = StackState<ProjectFeature.State>()
}
enum Action: Equatable {
case addButtonTapped
case path(StackAction<ProjectFeature.State, ProjectFeature.Action>)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .addButtonTapped:
// TODO: Handle action
return .none
case .path:
return .none
}
}
.forEach(\.path, action: /Action.path) {
ProjectFeature()
}
}
}
struct ProjectsView: View {
let store: StoreOf<ProjectsFeature>
var body: some View {
NavigationStackStore(self.store.scope(state: \.path, action: { .path($0) })) {
WithViewStore(self.store, observe: \.projects) { viewStore in
NavigationSplitView {
List {
ForEach(viewStore.state) { project in
NavigationLink(state: ProjectFeature.State(project: project)) {
Text(project.name)
}
}
}
} detail: {
Text("How do I get ProjectView() with Blob Jr to show here?")
}
}
} destination: { store in
ProjectView(store: store)
}
}
}
ProjectFeature is just like this:
But I wan't to be able to mutate the project from this view in the future.
struct ProjectFeature: Reducer {
struct State: Equatable {
var project: Project
}
enum Action {
case didUpdateNameTextField
}
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch(action) {
case .didUpdateNameTextField:
return .none
}
}
}
struct ProjectView: View {
let store: StoreOf<ProjectFeature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack {
Text("Project").font(.largeTitle)
Text(viewStore.state.project.name)
}
}
}
}
If I remove the NavigationSplitView, the navigation works, but the display is incorrect.
How can I use this NavigationSplitView with TCA?
eivindml
(Eivind Lindbråten)
2
I did get some help over at StackOverflow, and have updated ProjectsFeature and ProjectsView. Does this look like a OK solution? I guess there at some time might be a NavigationSplitViewStore that will make this easier.
One thing I would like to have help with is to move the selection state out of the view and into the feature state. How can I achieve this? 
struct Project: Equatable, Identifiable {
let id: UUID
var name: String
}
struct ProjectsFeature: Reducer {
struct State: Equatable {
var projects: IdentifiedArrayOf<Project> = []
@PresentationState var detail: ProjectFeature.State?
}
enum Action: Equatable {
case addButtonTapped
case didSelectItem(Project.ID?)
case detail(PresentationAction<ProjectFeature.Action>)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .addButtonTapped:
// TODO: Handle action
return .none
case .didSelectItem(let id):
if let project = state.projects.first(where: { $0.id == id }) {
state.detail = ProjectFeature.State(project: project)
} else {
state.detail = nil
}
return .none
case .detail(.dismiss):
return .none
case .detail(.presented(.didUpdateNameTextField)):
return .none
}
}
.ifLet(\.$detail, action: /Action.detail) {
ProjectFeature()
}
}
}
struct ProjectsView: View {
let store: StoreOf<ProjectsFeature>
@State private var selection: Project.ID?
var body: some View {
WithViewStore(self.store, observe: \.projects) { viewStore in
NavigationSplitView {
List(selection: $selection) {
ForEach(viewStore.state) { project in
Text("\(project.name)")
}
}
}
detail: {
IfLetStore(
store.scope(
state: \.$detail,
action: ProjectsFeature.Action.detail
)
) {
ProjectView(store: $0)
} else: {
// render a "no data available" view:
Text("Empty. Please select an item in the sidebar.")
}
}
.onChange(of: selection, { oldValue, newValue in
self.store.send(.didSelectItem(newValue))
})
}
}
}
dusiema
(Jens)
3
Searching for how to use a NavigationSplitView with TCA I found this thread and used your approach @eivindml.
Seems to work well. Did you ever come across any problems? (Thanks for posting it!)
Wonder if there will be a NavigationSplitView extension like there is one for NavigationStack already?