iOS开发:如何复用按钮与操作并让动画作用于对应图层而非仅最新添加图层
问题分析与解决方案
你遇到的问题核心很明确:你复用了全局的shapeLayer和percentageLabel变量,导致所有圆形都共享同一组图层/标签。每次调用createCircle时,你都会覆盖这个全局shapeLayer的位置属性,最终它只会绑定到最后一个创建的圆形位置。所以不管点击哪个按钮,修改的都是这个唯一的全局图层,自然只会在最后一个圆形上看到动画效果。
下面是最优雅的解决方式——把每个圆形的所有关联元素(进度轨道、进度图层、百分比标签、点击事件)封装到一个自定义UIView子类中,让每个圆形成为独立的实例,各自管理自己的状态:
步骤1:创建自定义进度圆形视图
这个视图会封装所有需要的元素和逻辑,确保每个圆形都是独立的个体:
import UIKit class ProgressCircleView: UIView { // 每个视图都有自己独立的图层和标签 private let trackLayer = CAShapeLayer() private let shapeLayer = CAShapeLayer() private let percentageLabel: UILabel = { let label = UILabel() label.text = "0%" label.textAlignment = .center label.font = UIFont.boldSystemFont(ofSize: 28) label.textColor = UIColor(red: 0.59, green: 0.42, blue: 0.23, alpha: 1.00) return label }() // 每个视图独立维护进度状态 private var done = 0 private let toDo = 5 override init(frame: CGRect) { super.init(frame: frame) setupUI() setupLayers() setupTapHandling() } required init?(coder: NSCoder) { super.init(coder: coder) setupUI() setupLayers() setupTapHandling() } private func setupUI() { addSubview(percentageLabel) percentageLabel.frame = bounds // 让标签铺满整个视图,居中显示 } private func setupLayers() { // 基于视图自身的 bounds 创建圆形路径,不用再手动传位置 let center = CGPoint(x: bounds.midX, y: bounds.midY) let radius = bounds.width / 2 - 5 // 减去线宽的一半,避免图层超出视图范围 let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true) // 背景轨道层 trackLayer.path = circularPath.cgPath trackLayer.strokeColor = UIColor(red: 0.82, green: 0.69, blue: 0.52, alpha: 1.00).cgColor trackLayer.fillColor = UIColor.clear.cgColor trackLayer.lineWidth = 10 layer.addSublayer(trackLayer) // 进度图层 shapeLayer.path = circularPath.cgPath shapeLayer.strokeColor = UIColor(red: 0.59, green: 0.42, blue: 0.23, alpha: 1.00).cgColor shapeLayer.fillColor = UIColor.clear.cgColor shapeLayer.lineWidth = 10 shapeLayer.lineCap = .round shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1) shapeLayer.strokeEnd = 0 // 初始进度为0 layer.addSublayer(shapeLayer) } private func setupTapHandling() { // 用手势识别器处理点击,也可以换成UIButton,逻辑一样 let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) addGestureRecognizer(tapGesture) isUserInteractionEnabled = true } @objc private func handleTap() { // 更新进度状态 if done < toDo { done += 1 } else { done = 0 // 重置进度 } // 计算百分比并更新UI let percentage = CGFloat(done) / CGFloat(toDo) percentageLabel.text = "\(Int(percentage * 100))%" // 动画更新进度条 UIView.animate(withDuration: 0.3) { self.shapeLayer.strokeEnd = percentage } } }
步骤2:修改ViewController代码
现在只需要创建这个自定义视图的实例,设置好位置添加到界面即可:
import UIKit class SecondViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white // 建议设置背景色,避免和进度条颜色冲突 // 定义所有圆形的位置 let circlePositions = [ CGPoint(x: 100, y: 200), CGPoint(x: 100, y: 375), CGPoint(x: 100, y: 550), CGPoint(x: 100, y: 725), CGPoint(x: 300, y: 200), CGPoint(x: 300, y: 375), CGPoint(x: 300, y: 550), CGPoint(x: 300, y: 725) ] // 创建并添加每个进度圆形 for position in circlePositions { let circleView = ProgressCircleView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) circleView.center = position view.addSubview(circleView) } } }
为什么这个方案有效?
- 独立性:每个
ProgressCircleView都拥有自己的trackLayer、shapeLayer和percentageLabel,彼此完全独立,不会互相覆盖。 - 内聚性:点击逻辑和进度管理都封装在视图内部,代码更清晰,后续维护(比如修改进度颜色、调整动画时长)也更方便。
- 灵活性:如果后续需要给圆形添加更多功能(比如长按重置、拖拽移动),直接在自定义视图里扩展即可,不用修改ViewController的代码。
如果你暂时不想用自定义视图,也可以用UIButton的tag或者关联对象把每个按钮对应的shapeLayer和percentageLabel绑定起来,但这种方式代码会更零散,不如自定义视图的方案优雅。
内容的提问来源于stack exchange,提问作者Blake Byers
相关产品推荐
相关产品推荐

