Reordering views in stacks while maintaining structural identity

I'm wondering what the easiest way to reordering a view in a stack is whilst maintaining structural identity, so that it animates/transitions correctly + state is maintained.

Ideally if in a LazyVStack it would maintain the laziness too.

Think of a stack where if a normal ForEach was used the view may end up being a switch over up to 20 different types of views.

I know custom Layouts can be used but they currently cannot be lazy and it seems like a fair bit of work for something that seems fairly simple.

Can the below approach be optimised?

import SwiftUI

struct GroupDemoView: View {
  @State private var animatesChanges = false
  @State private var shouldReorder = false

  private let reorderAnimation = Animation.spring(duration: 3, bounce: 0.5)

  var body: some View {
    VStack(spacing: 28) {
      VStack(spacing: 12) {
        Toggle("Animate changes", isOn: $animatesChanges)
        Toggle("Reorder subviews", isOn: animatesChanges ? $shouldReorder.animation(reorderAnimation) : $shouldReorder)
      }
      .frame(maxWidth: 240)

      ScrollView {
        ReorderingVStack(shouldReorder: shouldReorder) {
          Text("A")
          SimpleCard(name: "Card B")
          Text("B")
          Text("C")
          Text("D")
          Text("E")
          Text("F")
          Text("G")
          Text("H")
          Text("I")
          Text("J")
          Text("K")
        }
      }
      .font(.title3.weight(.semibold))
    }
    .padding()
  }
}

private struct ReorderingVStack<Content: View>: View {
  let shouldReorder: Bool
  let content: Content

  init(
    shouldReorder: Bool,
    @ViewBuilder content: () -> Content
  ) {
    self.shouldReorder = shouldReorder
    self.content = content()
  }

  var body: some View {
    Group(subviews: content) { subviews in
      VStack(spacing: 12) {
        ForEach(rearranged(subviews)) { subview in
          subview
        }
      }
    }
  }

  private func rearranged(_ subviews: SubviewsCollection) -> [Subview] {
    var subviews = Array(subviews)
    if shouldReorder {
      subviews.insert(subviews.remove(at: 1), at: 9)
    }
    return subviews
  }
}
Answered by Frameworks Engineer in 892382022

This example looks mostly correct.

When trying to ensure correct animations and transitions, you want to make sure your Subviews have stable identity. In this example, because the closure passed to ReorderingVStack's content is static, each view's identity is its position within it.

ForEach should be able to rearrange these subviews and animate them correctly. It's compatible with eager layouts like VStack, and lazy layouts like LazyVStack, so you can freely choose between them, if you want to support laziness. The layout will decide which views to load from the ForEach.

If you're trying to support drag based reordering, consider using the new reorderable API. It will take care of the ordering of subviews, the animations, add the drag gestures, and drop logic. It works in both eager and lazy contexts.

If not, you should consider making your rearranged method lazy by wrapping SubviewsCollection in a transforming collection. As it is right now, these collection mutations are going to run on every view body update, which can get expensive quickly for more complex operations. This is a pitfall that the reorderable API can help you avoid.

Hi,

This is an interesting case! One suggestion I can provide is keeping your data logic separate from the view -- think of your view as a direct description of your data model. You ideally don't want to rearrange the subviews themselves but instead modify the underlying data structure, and have the view represent that change.

Even if you have several different types of views representing the data structure, your views themselves can read only the data that is necessary for them. Then, to maintain structural identity in the model, a separate computed variable (based off the original data source) can be used for when shouldReorder is true, and your view can use that computed data source instead in that case.

This example looks mostly correct.

When trying to ensure correct animations and transitions, you want to make sure your Subviews have stable identity. In this example, because the closure passed to ReorderingVStack's content is static, each view's identity is its position within it.

ForEach should be able to rearrange these subviews and animate them correctly. It's compatible with eager layouts like VStack, and lazy layouts like LazyVStack, so you can freely choose between them, if you want to support laziness. The layout will decide which views to load from the ForEach.

If you're trying to support drag based reordering, consider using the new reorderable API. It will take care of the ordering of subviews, the animations, add the drag gestures, and drop logic. It works in both eager and lazy contexts.

If not, you should consider making your rearranged method lazy by wrapping SubviewsCollection in a transforming collection. As it is right now, these collection mutations are going to run on every view body update, which can get expensive quickly for more complex operations. This is a pitfall that the reorderable API can help you avoid.

Reordering views in stacks while maintaining structural identity
 
 
Q