SwiftUI 使用 GeometryEffect 实现卡片翻转显示不同内容的效果

839 阅读1分钟

原理请参见 Advanced SwiftUI Animations – Part 2: GeometryEffect

通过 GeometryEffect 协议实现自定义的 Modifier,通过实现并拦截协议中的 animatableData 变量(绑定为翻转角度),根据翻转角度绑定卡片翻转状态 flipped,以此来实现显示不同内容的效果:

struct FlipEffect: GeometryEffect {
  
  // SwiftUI 的动画就是通过时间函数操作 animatableData 变量,这里通过 getter setter 传递给给 angle 属性
  var animatableData: Double {
    get { angle }
    set { angle = newValue }
  }
  
  @Binding var flipped: Bool  // 绑定卡片当前的状态
  
  var angle: Double  // 翻转角度变量
  
  func effectValue(size: CGSize) -> ProjectionTransform {
    
    DispatchQueue.main.async {
      self.flipped = self.angle >= 90 && self.angle < 270  // 根据当前的角度改变 flipped 的值
    }
    
    let a = CGFloat(Angle.degrees(angle).radians)
    
    var transform3d = CATransform3DIdentity  // 对角单位矩阵
    
    transform3d = CATransform3DRotate(transform3d, a, 0, 1, 0)  // 以 (0, 1, 0) 为轴, (0, 0, 0) 为锚点旋转 a 度
    
    transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)  // 左移 1/2 width
    
    let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))  // 定义整体的右移 1/2 width 的效果,这样等效于中轴线旋转
    
    return ProjectionTransform(transform3d).concatenating(affineTransform)  // 组合应用上面的效果
  }
  
}

使用 Demo:

import SwiftUI

struct FlipEffectView: View {
  
  @State var flipped: Bool = false
  @State var trigger: Bool = false
  
  var body: some View {
    VStack {
      Text(flipped ? "反面" : "正面")
        .rotation3DEffect(.degrees(flipped ? 180 : 0), axis: (x: 0, y: 1, z: 0))
        .padding(24)
    }
    .background()
    .cornerRadius(16)
    .shadow(color: Color(.displayP3, red: 0, green: 0, blue: 0, opacity: 0.2), radius: 16, x: 0, y: 16)
    .modifier(FlipEffect(flipped: $flipped, angle: trigger ? 180: 0))
    .onTapGesture  {
      withAnimation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.5)) {
        self.trigger.toggle()
      }
    }
    
  }
}

struct FlipEffectView_Previews: PreviewProvider {
  static var previews: some View {
    FlipEffectView()
  }
}

效果:

FlipCard.gif