自定义环形进度视图在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
相关产品推荐
相关产品推荐

