导读:本期聚焦于盲改大师创作的《SwiftUI中如何自定义AnimatedProgressRing与CircularProgress实现动感环形进度条?》,敬请观看详情。在构建数据看板或文件上传界面时,系统自带的ProgressView常常无法满足视觉要求,尤其需要环形进度和动画过渡。通过SwiftUI自定义AnimatedProgressRing组件,你可以完全掌控环形绘制的细节,比如圆角端点、渐变描边以及进度值文本同步变化。本文从Shape的trim裁剪原理讲起,逐步拆解AnimatableModifier实现动画的核心机制,并给出可直接复用的CircularProgress完整代码。你会看到如何用单一路径配合stroke完成底层绘制,如何让进度值变化自动驱动动画,以及如何避免布局中的常见坑。读完这篇文章,你不仅能做出流畅的环形加载效果,还能把这些技巧迁移到仪表盘、倒计时等交互场景中。

环形进度条在日常移动应用中几乎随处可见,从健康应用里的活动圆环到上传文件时的加载指示,一个视觉细腻的环形动画能直接提升界面的完成度。SwiftUI本身提供了ProgressView,但默认样式是线性的,想要得到真正的圆环需要借助Shape和动画机制。本文将从一个基础实现出发,逐步加入动画和进度文本,构建一个可以灵活配置的AnimatedProgressRing组件。

SwiftUI中如何自定义AnimatedProgressRing与CircularProgress实现动感环形进度条?

环形绘制的基础:Shape与trim

SwiftUI绘制环形最简单的思路是使用Circle搭配trim修饰符。Circle本身是一个完整的圆,trim可以只显示其中一部分路径。比如下面这行代码绘制了一个从顶部开始、长度为百分之三十的圆弧:

Circle()
    .trim(from: 0.0, to: 0.3)
    .stroke(Color.blue, style: StrokeStyle(lineWidth: 20, lineCap: .round))
    .rotationEffect(.degrees(-90))

这里trim的from和to都是0到1之间的值,表示路径的起止位置,默认起点是圆的右侧即三点钟方向。开发者通常希望进度从顶部十二点方向开始,所以需要配合rotationEffect旋转负90度。stroke里的StrokeStyle可以设置线宽和端点样式,lineCap设为.round就能得到圆润的端点,适合进度条这种不完全闭合的图形。

如果只是静态展示,到这一步就够了,但真正的难点在于动画。SwiftUI不会自动对trim变化添加动画,因为trim的from和to并不是一个统一的属性。要让进度值变化时圆弧平滑增长或缩短,需要让SwiftUI知道这个值是一个可动画的量。直接使用withAnimation更改to值,效果可能是跳跃式的,因为Shape没有实现animatableData。接下来就需要引入自定义AnimatableModifier。

实现AnimatedProgressRing:AnimatableModifier驱动动画

AnimatableModifier是SwiftUI中一种特殊的ViewModifier,它要求提供animatableData属性,SwiftUI会在动画过程中对该属性进行插值。我们可以创建一个结构体,让它保存进度值,并把该值声明为animatableData。当外部progress变化并且包裹在withAnimation中时,SwiftUI会通过AnimatableModifier对animatableData进行线性插值,从而让Shape重新计算trim的to值,实现平滑动画。

struct ProgressRingModifier: AnimatableModifier {
    var progress: CGFloat
    var lineWidth: CGFloat
    var lineColor: Color
    var trackColor: Color
    
    var animatableData: CGFloat {
        get { progress }
        set { progress = newValue }
    }
    
    func body(content: Content) -> some View {
        ZStack {
            Circle()
                .stroke(trackColor, style: StrokeStyle(lineWidth: lineWidth))
            Circle()
                .trim(from: 0.0, to: min(progress, 1.0))
                .stroke(lineColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
                .rotationEffect(.degrees(-90))
        }
        .padding(lineWidth / 2)
    }
}

注意animatableData的setter里我们直接修改progress,这样body每次重新计算时就会得到插值后的中间数值,trim自然过渡。使用min(progress, 1.0)是为了防止progress超过1导致路径异常。外层ZStack把进度环叠加在灰色底环上,底环可以让用户感知整个进度区间。padding的作用是防止圆环因为圆角端点被裁剪,因为stroke默认会在边界绘制,加上圆角端点后实际范围会向外扩半个线宽。

外部使用这个Modifier时只需调用modifier方法,注意SwiftUI的modifier放在视图链末尾。如果希望动画可配置,还可以在modifier中提供animation参数,让调用方决定使用easeInOut还是spring。下面展示如何在一个View中使用AnimatedProgressRing:

struct ContentView: View {
    @State private var progress: CGFloat = 0.25
    
    var body: some View {
        VStack(spacing: 40) {
            AnimatedProgressRing(progress: progress)
                .frame(width: 200, height: 200)
            
            Slider(value: $progress, in: 0...1)
                .padding(.horizontal)
            
            Button("随机更新") {
                withAnimation(.easeInOut(duration: 1.2)) {
                    progress = CGFloat.random(in: 0...1)
                }
            }
        }
    }
}

AnimatedProgressRing本身是一个View,内部使用Circle和Modifier组合。把frame限制为固定尺寸很重要,否则圆环可能被拉伸成椭圆。Slider和Button用于测试动画是否对progress变化生效。点击按钮触发withAnimation后,你会发现圆弧不再瞬间跳变,而是流畅地扫过对应角度,这正是AnimatableModifier在幕后插值的结果。

CircularProgress:增加渐变、圆角端点和进度文本

生产环境中往往希望进度环更有质感,一个常见的需求是使用渐变描边代替纯色。SwiftUI的stroke方法支持使用AngularGradient或者LinearGradient,配合trim同样可以工作,但要注意渐变方向是否与旋转一致。更稳健的做法是用fill替换stroke,但填充会得到扇形而非环形。因此继续使用stroke,把渐变色作为第一个参数传入。为了让渐变跟随路径角度,AngularGradient是更合适的选择。

进度文本的展示也不可或缺。我们可以把文本放在ZStack中心,使用overlay或者直接在AnimatedProgressRing内部叠加一个Text。文本内容需要显示百分数,可以取progress乘以100后去掉小数。为了避免文本布局影响圆环,建议使用fixedSize或者给一个固定宽度。下面的CircularProgress组件整合了渐变圆环、灰色底环、圆角端点和进度文本:

struct CircularProgress: View {
    var progress: CGFloat
    var lineWidth: CGFloat = 20
    var startColor: Color = .blue
    var endColor: Color = .purple
    
    var body: some View {
        ZStack {
            Circle()
                .stroke(Color.gray.opacity(0.2), style: StrokeStyle(lineWidth: lineWidth))
            
            Circle()
                .trim(from: 0.0, to: min(progress, 1.0))
                .stroke(
                    AngularGradient(
                        gradient: Gradient(colors: [startColor, endColor]),
                        center: .center,
                        startAngle: .degrees(-90),
                        endAngle: .degrees(270)
                    ),
                    style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
                )
                .rotationEffect(.degrees(-90))
            
            Text("\(Int(progress * 100))%")
                .font(.system(size: 34, weight: .bold, design: .rounded))
                .foregroundColor(.primary)
        }
        .padding(lineWidth / 2)
        .modifier(ProgressRingModifier(progress: progress, lineWidth: lineWidth))
    }
}

注意这里的实现同时使用了两个Circle:一个灰色底环和一个渐变进度环。渐变进度环通过trim控制显示长度,并应用了ProgressRingModifier让progress变化具有动画插值。但这种写法下modifier内部的body又绘制了底环和进度环,与外部写好的两个Circle会发生重复。实际上更好的做法是把所有绘制逻辑移到Modifier中,外部View只负责承载Modifier。之所以在CircularProgress中再次书写,是为了演示可以灵活叠加文本;真正的动画依然依靠Modifier中的animatableData。为避免混淆,建议把底环、进度环和渐变色全部封装进一个独立的Shape或者Modifier,外部只传递progress和样式参数。

如果想在文本上也加上动画,SwiftUI的Text不会自动跟随Modifier中的插值更新,因为进度文本是外部状态直接读取的。要实现数字滚动效果,需要在外部用Timer或绑定到animatableData,或者简单地在onChange中更新文本。实际项目中更推荐创建一个专门显示数字动效的视图,用AnimatableModifier或ContentTransition,但为了保持示例简洁,CircularProgress中的Text会在进度变化时瞬间更新,配合圆环动画仍然可以接受。

进阶优化与常见坑点

第一个坑是圆环尺寸与padding的关系。stroke的圆角端点样式会让实际绘制区域略大于原本的frame,如果不加padding,圆环边缘可能被父视图裁剪。特别是在使用List或LazyVStack时,系统裁剪可能导致渐进部分缺失。因此务必给环形视图增加lineWidth一半的padding,或者在使用frame时留出余量。另一个容易被忽略的是旋转之前的坐标原点:swiftUI的Circle默认从右侧开始,旋转负90度后起点变为顶部,这一点适用于大多数进度场景,但如果需要从其他角度起始,可以调整rotationEffect的角度。

性能方面,AnimatableModifier在动画过程中频繁重建视图,如果圆环尺寸很大或者叠加了复杂的渐变、阴影,可能会对CPU造成压力。对于简单的环形进度,这种开销可以忽略不计;但在列表中出现多个环形进度条时,建议使用Canvas或Metal进一步优化。另外一些开发者尝试使用Shape协议直接实现animatableData,其实本质和Modifier类似,但Shape方式需要实现path(in:)函数并确保在动画插值时返回正确路径,代码量更大。本文的Modifier方案足够应对大多数需求,且与View体系结合更自然。

最后需要注意的是跨版本兼容性。SwiftUI的某些动画行为在iOS 15之前略有差异,如果项目需要支持较早版本,建议测试easeInOut动画是否正常。另外Min(progress, 1.0)的写法可以防止越界,但当progress为负数时trim会直接裁剪为零,最好在外部约束progress范围。通过本文介绍的AnimatedProgressRing和CircularProgress思路,你可以快速在项目里搭建出美观且流畅的环形进度指示器,为加载、下载、倒计时等场景提供直观反馈。

SwiftUIAnimatedProgressRingCircularProgress修改时间:2026-09-18 03:59:12

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0918/58665.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。