Combining recursive Reducer and NavigateAndLoad

Hey TCA community :)

I have a very simple use case which I want to realise with the composable architecture.
Imagine remotely browsing a folder structure where the content of a folder is a loaded as soon as you navigate to it.

I digged through different threads here, mainly Recursive reducer on optional sub-states and Recursive navigation - #6 by maximkrouk and tried to implement a similar approach.
You can find my test project on Github: GitHub - j0h4nn3s/RecursiveBrowsingComposableArchitecture: Test projects to test recursive browsing with the composable architecture.

It looks like I am missing something; when I am navigating deeper than three levels I receive an unexpected action (which resets the navigation stack in the state and therefor breaks the app) but I cannot figure out where it is coming from.

I expect to receive a navigate action from the third level:

FolderAction.subFolder(
    FolderAction.subFolder(
      FolderAction.navigateTo(
        9
      )
    )
  )

which I do in fact receive. But at the same time I also receive this action:

FolderAction.subFolder(
    FolderAction.navigateTo(nil)
  )

which I would only expect when popping something from the navigation stack.

Has anybody here implemented something similar and can see where I made a mistake? Any help is appreciated.

Edit: I think I figured it out. Seems to be this bug: Deep nested navigation - #2 by grinder81 and adding .navigationViewStyle(StackNavigationViewStyle()) to the NavigationView seems to do the trick.

NB: I just saw your edit before I was about to post the below :sweat_smile:. I'll still post it for posterity because I think some of it will help you out, but what you found is also another navigation "trick" we've had to employ to work around strange popping behavior.


Hey @GrafHubertus, when I run your application in Xcode 12.5.1 I don't see the behavior you are describing, but I know exactly what you are talking about. I have witnessed it many times myself, and there have been a few discussions on this forum and in the GitHub discussions about this issue.

It appears to be a SwiftUI bug. If you observe too much state in a screen it seems that NavigationLinks can get confused and pop their content. It's reproducible in even vanilla SwiftUI.

It's hard to know if this is the problem you are seeing because I can't reproduce locally, but I can tell you how chisel away the state that you are observing.

For example, right now your RootView is observing all of RootState even though it doesn't actually need any of that state in the body:

struct RootView: View {
  let store: Store<RootState, RootAction>
  var body: some View {
    WithViewStore(store) { viewStore in
      ...
    }
  }
}

This means every single little state change in the application, even if it's just in the 10th drill down screen of the folder structure, is going to cause this view to recompute its body.

One easy thing you can do here is just transform the store to be "stateless", which means it will not observe any state:

WithViewStore(store.stateless) { viewStore in

There is a similar problem in FolderView. You are observing all of FolderState even though all you need is content and selection.value?.item.id. In particular that means you are observing all state changes to every child FolderView, not just this one.

To fix this you can introduce a little struct to hold just the state the view cares about:

struct FolderView: View {
  let store: Store<FolderState, FolderAction>

  struct ViewState: Equatable {
    let content: IdentifiedArrayOf<Item>?
    let selectionItemId: Int?

    init(state: FolderState) {
      self.content = state.content
      self.selectionItemId = state.selection.value?.item.id
    }
  }

  ...
}

And then .scope the store before hitting it with WithViewStore:

WithViewStore(store.scope(state: ViewState.init)) { viewStore in
  ...
}

And then the only change you need to make in the view's body is to use selectionItemId rather than reaching into the selection directly:

- selection: viewStore.binding(get: \.selection.value?.item.id, send: FolderAction.navigateTo)
+ selection: viewStore.binding(get: \.selectionItemId, send: FolderAction.navigateTo),

I'd be curious if any of that helps. If not, then I'd also be curious what version of Xcode/iOS you are testing on where you see this behavior.

1 Like

Hey @mbrandonw, thank you sooo much for your reply! I am sorry for my edit, I should have given myself more time for debugging before posting here. I really admire the effort you are putting into helping other random people on the internet and how quick you are to reply. You are a real role model!

I pushed the change with the navigationViewStyle modifier to Github when I made the edit, that's probably why you were not able to reproduce the behaviour I was describing.

Anyways, your comments regarding the scoping of state are extremely helpful. I never really thought about this and I think I have this issue in multiple views in projects I am working on (I even stumbled upon the stateless property once and wondered what it might be used for). Thank you!

Edit: I updated the example on Github with the suggested changes.

1 Like