Using NavigationSplitView with TCA

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?

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? :blush:

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

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?