SwiftUI

SwiftUI GeometryEffect

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