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

如何在SwiftUI中控制动画进度(类似UIKit的fractionComplete)

SwiftUI 手动控制动画进度(类似UIKit的UIViewPropertyAnimator.fractionComplete)

问题描述

在SwiftUI中实现视图在两种状态间的动画时,如何摆脱仅在状态间切换的限制,实现类似UIKit中UIViewPropertyAnimator的fractionComplete功能,手动设置特定动画进度?比如跟随滑块值、Socket数据或滚动视图偏移量(0-1的CGFloat)来静态呈现对应进度的状态,而非自动播放完整动画。

现有点击切换状态的参考代码:

struct TransitionView: View {
    @Namespace var namespace
    @State var isExpanded: Bool = false
    
    @ViewBuilder
    var body: some View {
        VStack {
            switch isExpanded {
            case true:
                VStack {
                    Circle()
                        .strokeBorder(Color.green, lineWidth: 5)
                        .matchedGeometryEffect(id: "circle", in: namespace)
                        .frame(width: 70, height: 70)
                    Text(verbatim: "I am Circle")
                        .matchedGeometryEffect(id: "text", in: namespace)
                }
            case false:
                HStack {
                    Circle()
                        .strokeBorder(Color.red, lineWidth: 5)
                        .matchedGeometryEffect(id: "circle", in: namespace)
                        .frame(width: 30, height: 30)
                    Text(verbatim: "I am Circle")
                        .matchedGeometryEffect(id: "text", in: namespace)
                }
            }
        }
        .contentShape(Rectangle())
        .animation(.easeInOut, value: isExpanded)
        .onTapGesture {
            isExpanded.toggle()
        }
    }
}

原动画效果

解决方案

核心思路是用0-1范围的进度变量替代布尔状态,手动计算所有过渡属性的插值,从而实现任意进度的静态状态呈现。

修改后的完整代码

struct TransitionView: View {
    @Namespace var namespace
    @State var progress: CGFloat = 0 // 0 = 未展开状态,1 = 完全展开状态
    
    // 计算圆的大小插值:从30到70平滑过渡
    private var circleSize: CGFloat {
        30 + (70 - 30) * progress
    }
    
    // 计算颜色插值:从红色到绿色平滑过渡
    private var circleColor: Color {
        Color.red.interpolated(to: .green, fraction: progress)
    }
    
    var body: some View {
        VStack(spacing: 20) {
            // 用ZStack实现布局的平滑过渡,避免切换容器的生硬感
            ZStack(alignment: .leading) {
                Circle()
                    .strokeBorder(circleColor, lineWidth: 5)
                    .frame(width: circleSize, height: circleSize)
                    .matchedGeometryEffect(id: "circle", in: namespace)
                
                Text("I am Circle")
                    .matchedGeometryEffect(id: "text", in: namespace)
                    // 根据进度计算垂直偏移,实现从右侧到下方的布局过渡
                    .offset(x: (1 - progress) * 40, y: progress * 80)
            }
            .frame(height: 100) // 预留足够空间容纳两种布局
            
            // 测试用滑块:实际场景可替换为滚动偏移、Socket数据等数据源
            Slider(value: $progress, in: 0...1)
                .padding(.horizontal, 20)
        }
        .animation(.easeInOut, value: progress)
        .contentShape(Rectangle())
    }
}

// 扩展Color实现RGB颜色插值
extension Color {
    func interpolated(to other: Color, fraction: CGFloat) -> Color {
        let clampedFraction = fraction.clamped(to: 0...1)
        let selfComponents = self.components
        let otherComponents = other.components
        
        return Color(
            red: selfComponents.red + (otherComponents.red - selfComponents.red) * clampedFraction,
            green: selfComponents.green + (otherComponents.green - selfComponents.green) * clampedFraction,
            blue: selfComponents.blue + (otherComponents.blue - selfComponents.blue) * clampedFraction,
            opacity: selfComponents.opacity + (otherComponents.opacity - selfComponents.opacity) * clampedFraction
        )
    }
    
    // 提取Color的RGB和透明度组件
    private var components: (red: CGFloat, green: CGFloat, blue: CGFloat, opacity: CGFloat) {
        let uiColor = UIColor(self)
        var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0
        uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        return (red, green, blue, alpha)
    }
}

关键实现细节

  • 进度变量替换布尔状态:用progress: CGFloat(0-1范围)代替isExpanded: Bool,直接映射动画的中间进度
  • 属性插值计算:对需要过渡的属性(大小、颜色、位置)手动计算插值,确保任意进度下都有对应的静态状态
  • 平滑布局过渡:用ZStack结合offset实现布局的连续过渡,避免切换VStack/HStack带来的生硬感
  • 灵活数据源适配:测试用的滑块可直接替换为滚动视图偏移量、Socket推送的进度值等外部数据源

内容的提问来源于stack exchange,提问作者bodich

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 19:59:53