GeometryEffect
GeometryEffect 是一个遵循 Animatable 和 ViewModifier 协议的协议,它可以对视图的坐标进行变化,并且不会影响到该视图的父视图和子视图的布局。
func effectValue(size: CGSize) -> ProjectionTransform
它返回的是变化效果的当前值,比如示例所示的,我们需要实现一个翻转效果:
struct FlipEffect: GeometryEffect { /// 是否已翻转 @Binding var flipped: Bool /// 翻转角度 var angle: CGFloat var animatableData: CGFloat { get { angle } set { angle = newValue } } func effectValue(size: CGSize) -> ProjectionTransform { DispatchQueue.main.async { // 获取当前变化的状态 // 通过绑定值将翻转状态回传给视图 flipped = angle >= .pi * 0.5 } // 返回的视图变化效果,这里是3D翻转效果 var transform3d = CATransform3DMakeRotation(angle, 0, 1, 0) transform3d = CATransform3DTranslate(transform3d, -size.width * 0.5, 0, 0) return ProjectionTransform(transform3d) } }
然后我们通过 modifier 调用:
@State private var angle: CGFloat = 0 @State private var flipped = false private let circleSize: CGFloat = 80 var randomString: String { Bool.random() ? "❤️" : "😭" } VStack { HStack(spacing: 20) { circle circle circle } HStack(spacing: 20) { Button("翻转") { withAnimation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.5) { angle += flipped ? -.pi : .pi } } .padding() Button("慢放") { withAnimation(.easeInOut(duration: 3)) { angle += flipped ? -.pi : .pi } } .padding() } } var circle: some View { ZStack { Circle() .fill(Color.blue) .frame(width: circleSize, height: circleSize) Text(flipped ? randomString : "?") .foregroundColor(.white) .font(.largeTitle) } .modifier(FlipEffect(flipped: $flipped, angle: angle)) .offset(x: circleSize * 0.5, y: 0) }
另外,GeometryEffect 还有一个可选的方法:
func ignoredByLayout() -> _IgnoredByLayoutEffect<Self>
一般来讲,当我们对视图做变换的时候,它的布局信息(坐标、尺寸等)会跟着变化,如果我们不希望如此,就可以使用这个方法,它不会改变视图的布局信息,但是动画效果并不受影响。
matchedGeometryEffect
matchedGeometryEffect 是 iOS 14 新增的 modifier,使用简单,功能强大。我们直接结合示例和代码来看:
@State private var changed = false @Namespace private var ns VStack { if changed { VStack { Image("icon") .cornerRadius(10) .matchedGeometryEffect(id: "icon", in: ns) HStack { Text("如果你喜欢 Eul,请评价/分享") .font(.subheadline) Spacer() Link(destination: URL(string: "https://apps.apple.com/cn/app/eul/id154199 Label("", systemImage: "square.and.arrow.up } } .matchedGeometryEffect(id: "content", in: ns) } } else { HStack { Image("icon") .cornerRadius(10) .matchedGeometryEffect(id: "icon", in: ns) HStack { Text("如果喜欢 Eul,请评价/分享") .font(.subheadline) Spacer() Link(destination: URL(string: "https://apps.apple.com/cn/app/eul/id154199 Label("", systemImage: "square.and.arrow.up } } .matchedGeometryEffect(id: "content", in: ns) } } Button("点我") { withAnimation(.spring()) { changed.toggle() } } .padding() }
通过代码,我们可以看到,动画前后,视图的内容并没有变化,只是布局发生了变化,针对这样的场景,我们可以通过 matchedGeometryEffect 给它添加平滑的动画效果。
matchedGeometryEffect 需要标识符 id 来同步视图的布局,id 是定义在命名空间 namespace 中的,通常我们只需要指定这两个参数就足够了。
matchedGeometryEffect 方法还提供以下参数,作了解:
- properties : 从 source view 复制的属性,默认是 frame,还可使用 position、size
- anchor : 锚点,默认是 center
- isSource : 是否作为源视图,默认是 true