SwiftUI中点击按钮实现Page Curl动画的优雅方案探讨
实现SwiftUI简洁版Page Curl翻页动画
方案1:自定义动画Modifier + withAnimation
无需依赖UIKit组件,通过自定义GeometryEffect结合SwiftUI原生的withAnimation就能实现简洁的翻页触发逻辑,调用方式和原生动画完全一致。
步骤1:定义翻页几何效果
struct PageCurlEffect: GeometryEffect { var progress: Double // 0到1,控制翻页进度 let direction: CGFloat // 1=向右翻,-1=向左翻 var animatableData: Double { get { progress } set { progress = newValue } } func effectValue(size: CGSize) -> ProjectionTransform { // 添加透视效果增强真实感 var perspectiveTransform = CATransform3DIdentity perspectiveTransform.m34 = -1 / max(size.width, size.height) // 计算旋转+偏移的组合变换 let rotationAngle = progress * .pi / 2 let rotation = CATransform3DRotate(perspectiveTransform, rotationAngle, 0, direction, 0) let offsetX = direction * size.width * (1 - progress) let translation = CATransform3DMakeTranslation(offsetX, 0, 0) let combinedTransform = CATransform3DConcat(rotation, translation) return ProjectionTransform(combinedTransform) } }
步骤2:封装成ViewModifier
extension View { func pageCurl(progress: Double, direction: CGFloat = 1) -> some View { self.modifier(PageCurlModifier(progress: progress, direction: direction)) } } struct PageCurlModifier: ViewModifier { var progress: Double var direction: CGFloat func body(content: Content) -> some View { content .clipShape(Rectangle()) .geometryEffect(PageCurlEffect(progress: progress, direction: direction)) .opacity(1 - progress) // 翻页过程中逐渐透明 } }
步骤3:在视图中使用
struct PageCurlDemoView: View { @State private var isPageCurled = false var body: some View { ZStack { // 底层页面 Color.blue .frame(width: 300, height: 400) // 上层可翻页的页面 Color.red .frame(width: 300, height: 400) .pageCurl(progress: isPageCurled ? 1 : 0, direction: 1) Button("触发翻页") { withAnimation(.easeInOut(duration: 1.5)) { isPageCurled.toggle() } } .foregroundColor(.white) .padding() .background(.black.opacity(0.7)) .cornerRadius(8) } } }
方案2:轻量UIView封装(更逼真的阴影/褶皱)
如果纯SwiftUI的3D效果不够逼真,可以封装一个Core Animation实现的轻量UIView,通过UIViewRepresentable接入SwiftUI,同样支持withAnimation控制。
示例代码
struct PageCurlView: UIViewRepresentable { @Binding var progress: Double let direction: CGFloat func makeUIView(context: Context) -> PageCurlUIView { PageCurlUIView() } func updateUIView(_ uiView: PageCurlUIView, context: Context) { uiView.updateCurlProgress(CGFloat(progress), direction: direction) } } class PageCurlUIView: UIView { private let frontLayer = CALayer() override init(frame: CGRect) { super.init(frame: frame) setupLayers() } required init?(coder: NSCoder) { super.init(coder: coder) setupLayers() } private func setupLayers() { frontLayer.backgroundColor = UIColor.red.cgColor frontLayer.frame = bounds frontLayer.shadowColor = UIColor.black.cgColor frontLayer.shadowOpacity = 0.3 layer.addSublayer(frontLayer) } func updateCurlProgress(_ progress: CGFloat, direction: CGFloat) { let angle = progress * .pi / 2 var transform = CATransform3DIdentity transform.m34 = -1 / bounds.size.height transform = CATransform3DRotate(transform, angle, 0, direction, 0) transform = CATransform3DMakeTranslation(direction * bounds.width * (1 - progress), 0, 0) frontLayer.transform = transform frontLayer.opacity = Float(1 - progress) // 动态调整阴影偏移增强真实感 frontLayer.shadowOffset = CGSize(width: direction * 10 * progress, height: 5 * progress) } } // 使用方式 struct DemoView: View { @State private var curlProgress = 0.0 var body: some View { ZStack { Color.blue.frame(width: 300, height: 400) PageCurlView(progress: $curlProgress, direction: 1) .frame(width: 300, height: 400) Button("翻页") { withAnimation(.easeInOut(duration: 1.5)) { curlProgress = curlProgress == 0 ? 1 : 0 } } } } }
优化提示
- 调整
m34的值可以改变透视强度,绝对值越大透视效果越明显 - 若需要更精细的纸张褶皱效果,可以在翻页过程中动态绘制自定义Path作为裁剪区域
- 两种方案都支持自定义动画曲线(比如
.spring()),只需替换withAnimation的参数即可
内容的提问来源于stack exchange,提问作者Dmytro
相关产品推荐
相关产品推荐

