SwiftUI:如何将matchedGeometryEffect与自定义Transition结合?
如何为matchedGeometryEffect视图替换默认转场
我想给应用了matchedGeometryEffect()的视图替换默认的AnyTransition.opacity转场,但尝试多种方法都失败了。
原始实现(默认转场)
单独使用matchedGeometryEffect时,ItemView会通过默认动画在两个容器间"移动",但动画中途圆形会变浅,这是默认的透明度转场效果:
struct ContentView: View { @Namespace private var itemMovement @State private var isInFirst = true var body: some View { VStack { Text("First Container") HStack { if isInFirst { ItemView() .matchedGeometryEffect(id: "item", in: itemMovement) } } .padding() .border(.red) Text("Second Container") HStack { if !isInFirst { ItemView() .matchedGeometryEffect(id: "item", in: itemMovement) } } .padding() .border(.red) Button("Move Item") { withAnimation { isInFirst.toggle() } } } } } struct ItemView: View { var body: some View { Circle() .foregroundStyle(.blue) .frame(width: 100, height: 100) } }
失败的尝试
我尝试给视图显式添加AnyTransition.identity来移除默认转场,但结果是视图不再有matchedGeometryEffect的移动动画,直接跳转到目标位置:
struct ContentView: View { @Namespace private var itemMovement @State private var isInFirst = true var body: some View { VStack { Text("First Container") HStack { if isInFirst { ItemView() .transition(.identity) // 添加的转场 .matchedGeometryEffect(id: "item", in: itemMovement) } } .padding() .border(.red) Text("Second Container") HStack { if !isInFirst { ItemView() .transition(.identity) // 添加的转场 .matchedGeometryEffect(id: "item", in: itemMovement) } } .padding() .border(.red) Button("Move Item") { withAnimation { isInFirst.toggle() } } } } }
我试过调换.transition()和.matchedGeometryEffect()的顺序,也试过只在单个视图上加转场,都没用。我需要解决如何将自定义转场(比如基于rotation3DEffect()的转场)和matchedGeometryEffect结合的问题。
解决方案
问题出在matchedGeometryEffect和transition的作用顺序上:matchedGeometryEffect本质是通过同步两个视图的布局属性实现"移动"效果,而默认的转场会在视图添加/移除时叠加透明度动画,干扰了这个同步过程。
正确的做法是把transition应用到容器上,而非视图本身,同时给matchedGeometryEffect设置isSource参数来明确源视图和目标视图:
示例代码(移除默认透明度转场)
struct ContentView: View { @Namespace private var itemMovement @State private var isInFirst = true var body: some View { VStack { Text("First Container") HStack { if isInFirst { ItemView() .matchedGeometryEffect(id: "item", in: itemMovement, isSource: isInFirst) } } .padding() .border(.red) .transition(.identity) // 转场应用到容器 Text("Second Container") HStack { if !isInFirst { ItemView() .matchedGeometryEffect(id: "item", in: itemMovement, isSource: !isInFirst) } } .padding() .border(.red) .transition(.identity) // 转场应用到容器 Button("Move Item") { withAnimation { isInFirst.toggle() } } } } }
自定义转场示例(带3D旋转效果)
如果你想添加自定义转场(比如3D旋转),可以创建自定义AnyTransition,同样应用到容器上:
// 自定义3D旋转转场 extension AnyTransition { static var rotate3D: AnyTransition { .modifier( active: Rotate3DModifier(isActive: true), identity: Rotate3DModifier(isActive: false) ) } } struct Rotate3DModifier: ViewModifier { let isActive: Bool func body(content: Content) -> some View { content .rotation3DEffect( .degrees(isActive ? 180 : 0), axis: (x: 0, y: 1, z: 0) ) } } // 使用自定义转场的ContentView struct ContentView: View { @Namespace private var itemMovement @State private var isInFirst = true var body: some View { VStack { Text("First Container") HStack { if isInFirst { ItemView() .matchedGeometryEffect(id: "item", in: itemMovement, isSource: isInFirst) } } .padding() .border(.red) .transition(.rotate3D) // 应用自定义转场 Text("Second Container") HStack { if !isInFirst { ItemView() .matchedGeometryEffect(id: "item", in: itemMovement, isSource: !isInFirst) } } .padding() .border(.red) .transition(.rotate3D) // 应用自定义转场 Button("Move Item") { withAnimation(.easeInOut(duration: 1)) { isInFirst.toggle() } } } } }
关键原理
matchedGeometryEffect的isSource参数:明确哪个视图是动画的源,确保布局属性同步的优先级高于转场动画。- 转场应用到容器:避免直接修改
matchedGeometryEffect修饰的视图,防止转场动画覆盖布局同步的效果。 - 自定义转场通过
ViewModifier实现:可以灵活添加任意动画修饰符,同时不干扰matchedGeometryEffect的核心移动效果。
内容的提问来源于stack exchange,提问作者Ankur
相关产品推荐
相关产品推荐

