Preservation of sub-tree state when some super view changes its parent

I marked this topic properly as #off-topic as long as we don't have a clear answer in this thread.


There is a design I want to achieve in SwiftUI which is based on layout changes that are not just frame changes as the final result will have some animations, so the main layout change should remain without animations.

Basically I'm asking myself how can we take a sub-tree in SwiftUI and move it into a different super view (and re-layout if necessary) but also preserve its current state.

Here is the basic implementation that does NOT work.

struct ContainerView: View {
  @State var boolean = false

  init() {
    print("constructing ContainerView")
  }
  
  var body: some View {
    print(ContainerView.self)
    let value = "\(boolean)"
    return VStack {
      Button(action: { self.boolean.toggle() }, label: { Text(value) })
      FlippableStack(
        binding: $boolean,
        content: { NavigationView { A() } }
      )
      .border(Color.red)
    }
  }
}

struct FlippableStack<Content>: View where Content: View {
  @Binding var binding: Bool
  var content: () -> Content
  
  var body: some View {
    if binding {
      return AnyView(VStack(content: content))
    } else {
      return AnyView(HStack(content: content))
    }
  }
}

struct A: View {
  var body: some View {
    print(A.self)
    return NavigationButton(destination: B(), label: { Text("A") })
  }
}

struct B: View {
  var body: some View {
    print(B.self)
    return Text("B")
  }
}

This experiment view contains a button to flip the state and trigger an exchange of some parent stack, and it contains a NavigationView which has a simple A button as its root view and pushes a B text as the second view on the navigation stack.

What I want to achieve here is (1) push B to the navigation stack and then (2) press the button to exchange the underlying stack so that (3) the NavigationView would preserve its current state.

Right now however the NavigationView is re-rendered and presents again its root view A.

Since I don't have SwiftUI yet, I can do nothing but to be an armchair expert (sad face :cry: ).

Likely you'll need to treat both subtrees as separate entity that shares data (via EnvironmentObject?). Because the source of truth inside a subtree has no way of communication to the outside structure, the actual source of truth must then be outside both subtrees.

I think I know in which direction your thinking goes, but I'm not sure EnvironmentObject would be the solution for this problem. Furthermore we don't want the behavior EnvironmentObject gives us, we don't want to allow all views to access another tree of views.

I guess SwiftUI does some caching under the hood because it knows that is has not to update NavigationView if I only would like to change the title of the button in the above VStack, even if body is called in ContainerView. That's why there must be some technique to achieve what I want.

I have two guesses but I couldn't get neither of them to work:

  • move the NavigationView with its whole subtree into a reference inside ContainerView (for some reason Bindable that are nested inside a class will stop causing body of the view that is a class to be updated - not sure if it's a bug or by design)
  • there is a view called IDView which might be what we want here, but I have no idea how to properly use it, there is no examples out in the wild that uses it either

In UIKit I would just preserve a reference to the sub-tree I want and move it, but in SwiftUI we build value type views, at least that's what all people do and Apple tutorials shows us (including the framework types). I'm not sure what it would mean in SwiftUI context if you have a final class MyView: View.

That said I really want to solve that problem because I think "I need it" to build several designs that would swap out some parent view on trait changes. If there is a better way to do so, I'm open minded and would like to learn it.

I see. IDView is very interesting. It seems the only difference compared to regular View is Hashable id. My wild guess is that you only need to provide a unique hashable id, then SwiftUI will internally mark views with the same type and id as being the same view. Very likely the id needs to be unique across the object and used no more than once in every layout.

Did you ever find a good way to do this?