Hi everyone 👋
I’m fairly new to iOS development and I’ve been stuck on a SwiftUI issue for a while now, so I’m hoping someone here can spot what I’m doing wrong.
I’m using navigationTransition(.zoom) together with matchedTransitionSource to animate navigation between views. The UI consists of a grid of items (currently a LazyVGrid, though the issue seems unrelated to laziness). Tapping an item zooms it into its detail view, which is structurally the same view type and can contain further items.
All good expect that interactive swipe-back sometimes causes the item to disappear from the grid once the parent view is revealed. This only happens when dismissing via the drag gesture; it does not occur when using the back button.
I’ve attached a short demo showing the issue and the Swift file containing the relevant view code.
Is there something obvious I’m doing wrong with navigationTransition / matchedTransitionSource, or is this a known limitation or bug with interactive swipe-back?
Thanks in advance.
import SwiftUI
struct TestFileView: View {
@Namespace private var ns: Namespace.ID
let nodeName: String
let children: [String]
let pathPrefix: String
private func transitionID(for childName: String) -> String {
"Zoom-\(pathPrefix)->\(childName)"
}
private let columns = Array(repeating: GridItem(.flexible(), spacing: 12), count: 3)
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
Text(nodeName)
.font(.title.bold())
.padding(.bottom, 6)
LazyVGrid(columns: columns, spacing: 12) {
ForEach(children, id: \.self) { childName in
let id = transitionID(for: childName)
NavigationLink {
TestFileView(
nodeName: childName,
children: childrenFor(childName),
pathPrefix: "\(pathPrefix)/\(childName)"
)
.navigationTransition(.zoom(sourceID: id, in: ns))
} label: {
TestFileCard(title: childName)
.matchedTransitionSource(id: id, in: ns)
}
.buttonStyle(.plain)
}
}
}
.padding()
}
}
private func childrenFor(_ name: String) -> [String] {
switch name {
case "Lorem": return ["Ipsum", "Dolor", "Sit"]
case "Ipsum": return ["Amet", "Consectetur"]
case "Dolor": return ["Adipiscing", "Elit", "Sed"]
case "Sit": return ["Do", "Eiusmod"]
case "Amet": return ["Tempor", "Incididunt", "Labore"]
case "Adipiscing": return ["Magna", "Aliqua"]
case "Elit": return ["Ut", "Enim", "Minim"]
case "Tempor": return ["Veniam", "Quis"]
case "Magna": return ["Nostrud", "Exercitation"]
default: return []
}
}
}
struct TestFileCard: View {
let title: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Image(systemName: "square.stack.3d.up")
.symbolRenderingMode(.hierarchical)
.font(.headline)
Text(title)
.font(.subheadline.weight(.semibold))
.lineLimit(2)
.minimumScaleFactor(0.85)
Spacer(minLength: 0)
}
.padding(12)
.frame(maxWidth: .infinity, minHeight: 90, alignment: .topLeading)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
private struct TestRoot: View {
var body: some View {
NavigationStack {
TestFileView(
nodeName: "Lorem",
children: ["Ipsum", "Dolor", "Sit"],
pathPrefix: "Lorem"
)
}
}
}
#Preview {
TestRoot()
}