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

UIBezierPath在部分Frame下失效问题求助

问题排查与解决方案

你猜的没错!问题核心就是固定半径导致图形超出View边界:

  • 当View尺寸是375x667时,它的短边是375,一半是187.5,比你设置的固定半径100大,所以环形能完整显示在View内部;
  • 但换成120x120或74x74时,短边的一半分别是60和37,都小于100,这时候环形的直径(200)远大于View尺寸,整个图形都跑到View可视区域外面了,自然看不到。

下面是修复后的完整代码,已经适配任意尺寸的View,同时做了细节优化:

import UIKit

// 补全你代码中用到的扩展
extension Int {
    var degreesToRadians: CGFloat {
        return CGFloat(self) * .pi / 180
    }
}

extension TimeInterval {
    var time: String {
        let minutes = Int(self) / 60 % 60
        let seconds = Int(self) % 60
        return String(format: "%02d:%02d", minutes, seconds)
    }
}

class CountdownView: UIView {
    let timeLeftShapeLayer = CAShapeLayer()
    let bgShapeLayer = CAShapeLayer()
    var timeLeft: TimeInterval = 3783
    var endTime: Date?
    var timeLabel = UILabel()
    var timer = Timer()
    let strokeIt = CABasicAnimation(keyPath: "strokeEnd")

    func drawBgShape() {
        // 核心修改:用View短边的一半减去线宽的一半作为半径,确保环形完全在View内部
        let radius = min(frame.width, frame.height) / 2 - bgShapeLayer.lineWidth / 2
        bgShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
                                         radius: radius,
                                         startAngle: -90.degreesToRadians,
                                         endAngle: 270.degreesToRadians,
                                         clockwise: true).cgPath
        bgShapeLayer.strokeColor = UIColor.white.cgColor
        bgShapeLayer.fillColor = UIColor.clear.cgColor
        bgShapeLayer.lineWidth = 15
        layer.addSublayer(bgShapeLayer)
    }

    func drawTimeLeftShape() {
        let radius = min(frame.width, frame.height) / 2 - timeLeftShapeLayer.lineWidth / 2
        timeLeftShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
                                               radius: radius,
                                               startAngle: -90.degreesToRadians,
                                               endAngle: 270.degreesToRadians,
                                               clockwise: true).cgPath
        timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
        timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
        timeLeftShapeLayer.lineWidth = 15
        // 初始化stroke状态,避免动画异常
        timeLeftShapeLayer.strokeStart = 0
        timeLeftShapeLayer.strokeEnd = 0
        layer.addSublayer(timeLeftShapeLayer)
    }

    func addTimeLabel() {
        // 用bounds代替frame计算位置,适配View尺寸变化
        timeLabel.frame = CGRect(x: bounds.midX - 50, y: bounds.midY - 25, width: 100, height: 50)
        timeLabel.textAlignment = .center
        timeLabel.text = timeLeft.time
        timeLabel.textColor = .black
        addSubview(timeLabel)
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        backgroundColor = UIColor(white: 0.94, alpha: 1.0)
        drawBgShape()
        drawTimeLeftShape()
        addTimeLabel()

        strokeIt.fromValue = 0
        strokeIt.toValue = 1
        strokeIt.duration = 60
        timeLeftShapeLayer.add(strokeIt, forKey: nil)

        endTime = Date().addingTimeInterval(timeLeft)
        timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
    }

    @objc func updateTime() {
        if timeLeft > 0 {
            timeLeft = endTime?.timeIntervalSinceNow ?? 0
            timeLabel.text = timeLeft.time
            // 同步更新进度条,让进度和剩余时间对应(可选优化)
            timeLeftShapeLayer.strokeEnd = CGFloat(timeLeft / 3783)
        } else {
            timeLabel.text = "00:00"
            timer.invalidate()
        }
    }

    // 额外优化:View尺寸变化时重新绘制,适配布局调整
    override func layoutSubviews() {
        super.layoutSubviews()
        bgShapeLayer.removeFromSuperlayer()
        timeLeftShapeLayer.removeFromSuperlayer()
        timeLabel.removeFromSuperview()
        drawBgShape()
        drawTimeLeftShape()
        addTimeLabel()
    }
}

关键修改说明:

  • 自适应半径:替换固定半径为min(frame.width, frame.height) / 2 - lineWidth / 2,确保环形始终在View内部,还预留了线宽空间避免线条溢出;
  • 使用bounds坐标系:bounds是View自身的内部坐标系,比依赖父视图的frame更可靠,适配View位置变化;
  • 添加布局更新逻辑:重写layoutSubviews,在View尺寸变化时重新绘制内容,适配旋转屏幕、父视图布局调整等场景;
  • 初始化动画状态:给进度层设置初始的strokeStart和strokeEnd,避免动画启动异常。

这样修改后,不管你的View是多大尺寸,都能正常显示环形进度条啦!

内容的提问来源于stack exchange,提问作者FS.O6

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:54:54