How to manage ForEachStore with NavigationLink's binding?

Hi I want help with Navigation Binding
When using the ForEachStore how should I toggle the value at AppState?

struct AppState: Equatable {
  var contacts: [Contact] = []
  var editMode: EditMode = .inactive
  var selectedContactID = ""
}

import Contacts
struct Contact: Equatable {
  var value: CNContact = .init() // CNContact is from Contacts framework
  var isFavourite = false
}

enum ContactAction: Equatable {
  case nameChanged(String)
  case contactTapped
  case starTapped
}

My AppView will utilize a ForEachView(self.store) and initialize the ContactView

struct ContentView: View {
  
  let store: Store<AppState, AppAction>
  
  var body: some View {
    NavigationView {
      WithViewStore(self.store) { viewStore in
        List {
          ForEachStore(self.store.scope(state: \.contacts, action: AppAction.contact(index:action:))) { contactStore in
            ContactView(store: contactStore)
}}} }}
}

struct ContactView: View {
  let store: Store<Contact, ContactAction>
  
  var body: some View {
      WithViewStore(self.store) { viewStore in
        NavigationLink(
          destination: Text(viewStore.value.givenName),
          isActive: viewStore.binding(

/// Need help with the binding. It should be
// AppState.selectedContactID == viewStore.value.id

            get: (String) -> LocalState,
            send: ContactAction.contactTapped)
        ) {
          EmptyView()
  } } 
}

Possible Solutions:

  1. Use TypeAlias/Struct ContactState
typealias ContactState = (contacts: [Contact], selection: String)
  1. Have ForEachStore accept a binding for SwiftUI Navigation management
ForEachStore(self.store.contacts, binding: AppState.selection, toValue: \Contact.value.id)

Hi @Viranchee!

Do you plan on driving navigation from state programmatically? E.g. deep-linking? If not, you should be able to use the bindingless NavigationLink APIs and let SwiftUI handle it for you:

NavigationLink(
  destination: Text(viewStore.value.givenName)
) {
  EmptyView() // label here
}

If you do want to drive navigation with state programmatically, it's a little trickier. We have a couple examples of lists of navigation links in the demos that ship with the repo (example 1, example 2), but they may be slightly more complicated in that they are working with optional sub-state for the row that is selected.

If you can get away with not using the binding-based NavigationLink API, though, I'd recommend it. SwiftUI currently has a few bugs around programmatic navigation that can make it quite tricky:

  • iPhone-based navigation needs to apply the .navigationViewStyle(StackNavigationViewStyle()) modifier to the navigation view in order to get the right binding behavior when drilling multiple layers into a navigation stack.
  • It's not currently possible to deep link several layers into a navigation stack all at once. Instead you need the UI to tick once between each layer, which is also pretty tricky to manage.

We're hoping that these things get fixed in the upcoming betas.

4 Likes

Dang! I spent half a day debugging it while the solution was so simple
Using Bindingless navigation

If a View needs access to more than one AppState, what is a good way to solve the problem

typealias / struct LocalState = (int: Int, array: [Int])

struct LocalView: View {
let store: <LocalState, LocalAction>
}

or

typealias LocalState1 = Int
typealias LocalState2 = Array<Int>

struct LocalView: View {
let store1: Store<LocalState1, LocalAction>
let store2: Store<LocalState2, LocalAction>
}

A ForEachStore returns a Store<LocalState1, LocalAction>
What is a good way to convert to Store<(LocalState1, LocalState2), LocalAction>, which is used at the LocalView's initialization

As long as the more global store contains both of these states, you can use the scope method to pluck them out before passing it to LocalView:

LocalView(store: self.store.scope(state: { ($0.int, $0.array) })
1 Like

Hi @stephencelis
I checked in the examples, searched for every store.scope and found
0 Examples of scoping multiple states in TCA Github
1 Example of scoping multiple states in the PrimeTime app (2x iOS and macOS file)

Also, we could use an example of using UIVIewControllerRepresentable for binding between SwiftUI and UIKit, which makes it easy to adopt for starters

While there are no examples of destructuring states into tuples, we have a few examples of introducing ViewState structs to accomplish the same. For example: https://github.com/pointfreeco/swift-composable-architecture/blob/a90c48924fed1748c407c8032827572fe70a5b63/Examples/TicTacToe/Sources/Views-SwiftUI/GameSwiftView.swift#L6-L11

And then a property to "destructure" state into the view struct value: https://github.com/pointfreeco/swift-composable-architecture/blob/a90c48924fed1748c407c8032827572fe70a5b63/Examples/TicTacToe/Sources/Views-SwiftUI/GameSwiftView.swift#L84-L95

Finally, the scoping is done in the view: https://github.com/pointfreeco/swift-composable-architecture/blob/a90c48924fed1748c407c8032827572fe70a5b63/Examples/TicTacToe/Sources/Views-SwiftUI/GameSwiftView.swift#L21

Structs can require a little more ceremony than tuples, but it's a trade-off that we've landed at recommending for now!

Thanks for the example! We're definitely interested in improving the demos and will think about this for the future :smile:

1 Like

:heart: You just made my day! Again! .navigationViewStyle(StackNavigationViewStyle()) seems to fix most of my problems with navigation in SwiftUI. I am aware it's not really a declarative UI (because of the lack of deterministic state restoration possibility), but at least I can use it for basic navigation. Without StackNavigationViewStyle it just deactivates links for no reason and crashing.

1 Like