You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 03:17:04