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

自定义环形进度视图在awakeFromNib中失效的原因与解决方案

问题描述

我用UIBezierPath和CAShapeLayer做了个环形进度视图,代码如下:

let center = self.center 
let trackLayer = CAShapeLayer() 
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true) 
trackLayer.path = circularPath.cgPath 
trackLayer.strokeColor = UIColor.lightGray.cgColor 
trackLayer.lineWidth = 10 
trackLayer.fillColor = UIColor.clear.cgColor 
trackLayer.lineCap = kCALineCapRound 
self.layer.addSublayer(trackLayer) 

shapeLayer.path = circularPath.cgPath 
shapeLayer.strokeColor = UIColor.red.cgColor 
shapeLayer.lineWidth = 10 
shapeLayer.fillColor = UIColor.clear.cgColor 
shapeLayer.lineCap = kCALineCapRound 
shapeLayer.strokeEnd = 0 
self.layer.addSublayer(shapeLayer) 

self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap))) 

这段代码放在控制器的viewDidLoad里运行完全正常,但我从Storyboard拖了个UIView,把它的类改成我自己写的自定义UIView类,然后把代码复制到这个类的awakeFromNib方法里,就完全不工作了。想问问这是为啥?还有该怎么在自定义UIView类里正确用这段代码?

另外我已经给这个橙色的自定义视图加了leading、trailing、bottom约束,还有固定高度约束,但显示效果不对。是不是环形视图得放在橙色视图的中心位置才行?


原因分析与解决办法

1. 为啥awakeFromNib里代码失效?

核心问题就是视图布局的时机不对:

  • 当awakeFromNib被调用时,你的自定义视图还没完成约束布局呢!self.center拿到的是Storyboard里的初始占位位置,不是最终布局后的真实中心坐标。用这个错误的中心去画环形,自然显示得乱七八糟。
  • 而在控制器的viewDidLoad里,控制器的视图已经完成了初始布局,self.center是正确的最终位置,所以代码能正常运行。

2. 自定义UIView的正确实现方式

你得把创建和更新环形路径的代码,放到视图布局完成后的回调方法里,这样才能拿到正确的视图尺寸和中心。推荐两种靠谱的写法:

写法一:重写layoutSubviews

这是UIView处理布局变化的核心方法,每次视图尺寸改变(包括第一次布局完成)都会被调用,非常适合用来更新依赖视图尺寸的图层:

class CircularProgressView: UIView {
    // 把图层定义成私有属性,避免外部随意修改
    private let trackLayer = CAShapeLayer()
    private let shapeLayer = CAShapeLayer()
    
    // 代码初始化的入口
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupBaseLayerConfig()
    }
    
    // Storyboard/XIB初始化的入口
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupBaseLayerConfig()
    }
    
    // 初始化图层的基础配置(不依赖视图尺寸的部分)
    private func setupBaseLayerConfig() {
        // 配置轨道图层
        trackLayer.strokeColor = UIColor.lightGray.cgColor
        trackLayer.lineWidth = 10
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineCap = kCALineCapRound
        self.layer.addSublayer(trackLayer)
        
        // 配置进度图层
        shapeLayer.strokeColor = UIColor.red.cgColor
        shapeLayer.lineWidth = 10
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineCap = kCALineCapRound
        shapeLayer.strokeEnd = 0
        self.layer.addSublayer(shapeLayer)
        
        // 添加点击手势
        self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
    }
    
    // 布局完成后更新路径
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // 计算当前视图的真实中心
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        // 计算安全半径:避免环形超出视图边界,要减去线宽的一半
        let radius = min(bounds.width, bounds.height) / 2 - trackLayer.lineWidth / 2
        // 创建环形路径
        let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
        
        // 更新两个图层的路径
        trackLayer.path = circularPath.cgPath
        shapeLayer.path = circularPath.cgPath
    }
    
    // 点击手势的处理方法
    @objc private func handleTap() {
        // 这里可以加进度动画的逻辑,比如从0到1的动画
        let progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
        progressAnimation.toValue = 1.0
        progressAnimation.duration = 1.0
        progressAnimation.fillMode = .forwards
        progressAnimation.isRemovedOnCompletion = false
        shapeLayer.add(progressAnimation, forKey: "progressAnimation")
    }
}

写法二:监听bounds变化

如果你需要更细粒度的控制,可以通过重写bounds的didSet方法来监听尺寸变化:

class CircularProgressView: UIView {
    private let trackLayer = CAShapeLayer()
    private let shapeLayer = CAShapeLayer()
    
    // 监听bounds变化,每次尺寸改变就更新路径
    override var bounds: CGRect {
        didSet {
            updateCircularPath()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupBaseLayerConfig()
        updateCircularPath()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupBaseLayerConfig()
        updateCircularPath()
    }
    
    private func setupBaseLayerConfig() {
        // 同写法一的基础配置逻辑
        trackLayer.strokeColor = UIColor.lightGray.cgColor
        trackLayer.lineWidth = 10
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineCap = kCALineCapRound
        self.layer.addSublayer(trackLayer)
        
        shapeLayer.strokeColor = UIColor.red.cgColor
        shapeLayer.lineWidth = 10
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineCap = kCALineCapRound
        shapeLayer.strokeEnd = 0
        self.layer.addSublayer(shapeLayer)
        
        self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
    }
    
    // 专门用来更新环形路径的方法
    private func updateCircularPath() {
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let radius = min(bounds.width, bounds.height) / 2 - trackLayer.lineWidth / 2
        let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
        
        trackLayer.path = circularPath.cgPath
        shapeLayer.path = circularPath.cgPath
    }
    
    @objc private func handleTap() {
        // 同写法一的点击逻辑
        let progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
        progressAnimation.toValue = 1.0
        progressAnimation.duration = 1.0
        progressAnimation.fillMode = .forwards
        progressAnimation.isRemovedOnCompletion = false
        shapeLayer.add(progressAnimation, forKey: "progressAnimation")
    }
}

3. 关于环形居中的问题

没错!环形视图必须放在橙色自定义视图的中心位置才行。上面的代码里用bounds.midX和bounds.midY计算的就是当前视图的真实中心,这样不管你的自定义视图因为约束发生什么尺寸变化(比如你加了leading、trailing约束,宽度会随父视图变化),环形都会始终居中,而且我们计算半径时减去了线宽的一半,还能避免环形超出视图的边界。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:02:59