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

Swift自定义视图CABasicAnimation的strokeEnd动画不生效

问题根因

CABasicAnimation描边动画不生效是几个代码细节错误叠加导致的,和你检查过的视图frame配置无关,核心是layer坐标系、属性初始值、布局时机的问题:

  • subLayer的frame赋值错误:你写的subLayer.frame = layer.frame逻辑有误,sublayer的frame是相对于父layer(即ToastView的根layer)的bounds坐标系,而layer.frame是根layer相对于它父视图的坐标,直接赋值会导致shapeLayer偏移出可视区域,根本看不到绘制的描边。
  • 没有设置shapeLayer的model层属性:CABasicAnimation只改动渲染层的临时展示值,动画执行过程中看到的效果都是渲染层输出的,如果你不给subLayer.strokeEnd设置最终值1,动画开始前/结束后渲染层会回到model层的默认值0,很容易出现完全看不到动画的情况。
  • 布局时机错误:如果调用startAnimation的时候ToastView还没完成约束布局,此时self.bounds是不准确的,用这个bounds画出来的贝塞尔路径尺寸、位置都是错的。
  • 重复添加sublayer:每次调用startAnimation都会执行一次insertSublayer,多次调用会堆叠大量重复的shapeLayer在视图层级里,引发渲染异常。
修复代码

调整layer的初始化和布局逻辑,把sublayer添加提前到初始化阶段,布局更新放到layoutSubviews里保证尺寸准确,动画部分补全model层赋值即可:

class ToastView: UIView {

    lazy var label: UILabel = {
        let label = UILabel()
        label.textColor = .white
        label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
        return label
    }()

    lazy var subLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = nil
        shapeLayer.strokeColor = UIColor.green.cgColor
        shapeLayer.lineCap = .round
        shapeLayer.lineWidth = 2
        // 初始strokeEnd设为0,默认不显示描边
        shapeLayer.strokeEnd = 0
        return shapeLayer
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        customInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        customInit()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        // 布局完成后更新subLayer的frame和path,保证尺寸准确
        subLayer.frame = layer.bounds
        subLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 4).cgPath
    }

    private func customInit() {
        backgroundColor = .black
        self.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        let constraints = [
            label.topAnchor.constraint(equalTo: self.topAnchor, constant: 2),
            label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 3),
            label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 4),
            label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 4)
        ]
        NSLayoutConstraint.activate(constraints)

        layer.borderWidth = 1
        layer.borderColor = UIColor.white.cgColor
        layer.cornerRadius = 4
        layer.masksToBounds = true
        // 只添加一次subLayer,不要每次动画都重复加
        layer.insertSublayer(subLayer, at: 0)
    }

    func startAnimation(toastMessage: String) {
        self.label.text = toastMessage
        // 强制更新布局,保证此时bounds是准确的
        self.layoutIfNeeded()
        
        // 先给model层设置最终值,避免动画跳变
        subLayer.strokeEnd = 1
        
        let strokeAnimation = CABasicAnimation(keyPath: "strokeEnd")
        strokeAnimation.fromValue = 0
        strokeAnimation.toValue = 1
        strokeAnimation.duration = 1.5
        strokeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        
        subLayer.add(strokeAnimation, forKey: "strokeEndAnimation")
        // 后续写弹出动画逻辑即可
    }
}
额外说明
  • 不需要手动设置beginTime = 0,这个是动画的默认值,写了没有实际作用。
  • 给动画设置非空的key,后续如果需要提前终止动画可以通过key找到并移除。
  • 只要你在添加动画前设置了subLayer.strokeEnd = 1,就不需要设置isRemovedOnCompletion = false和fillMode = .forwards来强行保留动画最后一帧,那种写法是不规范的,本质是让动画留在渲染层不更新model层,会导致后续属性读写异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 02:27:27