SwiftUI实现LazyHStack单元格到详情页的Instagram式转场动画
SwiftUI实现Instagram式故事转场(源视图保持不动)
你的问题出在:当前代码通过if-else切换主视图和详情页,导致选中的源单元格被从视图层级中移除,matchedGeometryEffect会将源视图的位置过渡到详情页,所以才会出现源单元格移动的现象。要让源视图保持原位,核心是保留源视图的存在,仅在详情页显示时隐藏选中的源单元格,同时让详情页的视图基于源单元格的位置做过渡动画。
以下是修改后的完整代码:
struct ContentView: View { @Namespace private var animationNamespace @State private var selectedIndex: Int? = nil var body: some View { ZStack { // 主视图始终保留在层级中 ScrollView(.horizontal) { LazyHStack(spacing: 10) { ForEach(0..<5) { index in RoundedRectangle(cornerRadius: 20) .fill(Color.blue) .frame(width: 80, height: 80) .matchedGeometryEffect(id: "container\(index)", in: animationNamespace) // 选中时隐藏源单元格,但不移除 .opacity(selectedIndex == index ? 0 : 1) .onTapGesture { withAnimation(.easeInOut(duration: 0.5)) { selectedIndex = index } } } } .padding() } // 详情页仅在选中时显示,覆盖在主视图上方 if let index = selectedIndex { DetailView(namespace: animationNamespace, selectedIndex: index) { withAnimation(.easeInOut(duration: 0.5)) { selectedIndex = nil } } } } } } struct DetailView: View { var namespace: Namespace.ID let selectedIndex: Int let closeClosure: (() -> Void) var body: some View { RoundedRectangle(cornerRadius: 20) .fill(Color.purple) .matchedGeometryEffect(id: "container\(selectedIndex)", in: namespace) .frame(maxWidth: .infinity, maxHeight: .infinity) .edgesIgnoringSafeArea(.all) .onTapGesture { closeClosure() } } } #Preview { ContentView() }
关键修改点说明:
- 保留主视图层级:移除了
if-else切换逻辑,让ScrollView始终存在于ZStack中,避免源视图被移除。 - 隐藏选中的源单元格:通过
opacity(selectedIndex == index ? 0 : 1)让选中的单元格透明,这样它依然占据原位置,但不可见,matchedGeometryEffect会以它的位置作为动画起点。 - 简化详情页布局:移除了多余的VStack,让详情页的矩形直接填充全屏,确保动画过渡到正确的结束状态。
这样修改后,点击单元格时,详情页会从源单元格的位置平滑放大到全屏,关闭时反向缩小,而源列表中的其他单元格和选中的单元格(隐藏但原位保留)都不会移动,完全符合Instagram故事的转场效果。
内容的提问来源于stack exchange,提问作者Xys
相关产品推荐
相关产品推荐

