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