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

设备方向改变时CoreGraphics自定义按钮重绘异常求助

Fixing Custom Button Shape Refresh on Device Rotation

Hey there! Let's tackle that orientation refresh issue with your RewardStepsButton. The core problem here is that every time draw(_:) runs (like when you call setNeedsDisplay()), you're adding a brand new CAShapeLayer to the button's layer stack. Over time, old layers stick around and cause the overlapping you're seeing. Here's how to fix this cleanly:

Step 1: Reuse a Single Shape Layer Instead of Creating New Ones

First, move your CAShapeLayer from the draw(_:) method to a class-level property. This way, you only create and add it to the button's layer once, then just update its content when needed.

Update your class definition like this:

class RewardStepsButton: UIButton { 
    @IBInspectable var firstStep: Bool = true{
        didSet{
            if firstStep{
                secondStep = false
                thirdStep = false
                setNeedsDisplay() // Refresh when state changes
            }
        }
    }
    @IBInspectable var secondStep: Bool = false{
        didSet{
            if secondStep{
                firstStep = false
                thirdStep = false
                setNeedsDisplay() // Refresh when state changes
            }
        }
    }
    @IBInspectable var thirdStep: Bool = false{
        didSet{
            if thirdStep{
                firstStep = false
                secondStep = false
                setNeedsDisplay() // Refresh when state changes
            }
        }
    }
    @IBInspectable var buttonColor: UIColor = UIColor.lightGray
    
    // Reusable shape layer (only created once)
    private let btnLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButton()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupButton()
    }
    
    private func setupButton() {
        // Add the shape layer once
        layer.addSublayer(btnLayer)
        // Ensure title label stays on top permanently
        bringSubviewToFront(titleLabel!)
    }
    
    // ... rest of your code
}

Step 2: Update the Layer in draw(_:) Instead of Adding New Layers

Modify your draw(_:) method to update the existing btnLayer's path, color, and frame—no more adding new layers every time. This eliminates the overlapping issue:

override func draw(_ rect: CGRect) {
    let path = UIBezierPath()
    
    if firstStep{
        path.addArc(withCenter: CGPoint(x: 5, y: 5), radius: 5, startAngle: .pi, endAngle: 3 * .pi / 2, clockwise: true)
        path.addLine(to: CGPoint(x: self.bounds.width - 15, y: 0))
        path.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height / 2))
        path.addLine(to: CGPoint(x: self.bounds.width - 15, y: self.bounds.height))
        path.addArc(withCenter: CGPoint(x: 5, y: self.bounds.height - 5), radius: 5, startAngle: .pi / 2, endAngle: .pi, clockwise: true)
        path.close()
    }else if secondStep{
        path.move(to: CGPoint(x: 0, y: 0))
        path.addLine(to: CGPoint(x: self.bounds.width - 15, y: 0))
        path.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height / 2))
        path.addLine(to: CGPoint(x: self.bounds.width - 15, y: self.bounds.height))
        path.addLine(to: CGPoint(x: 0, y: self.bounds.height))
        path.addLine(to: CGPoint(x: 15, y: self.bounds.height / 2))
        path.addLine(to: CGPoint(x: 0, y: 0))
    }else{
        path.move(to: CGPoint(x: 0, y: 0))
        path.addArc(withCenter: CGPoint(x: self.bounds.width - 5, y: 5), radius: 5, startAngle: 3 * .pi / 2, endAngle: 2 * .pi, clockwise: true)
        path.addArc(withCenter: CGPoint(x: self.bounds.width - 5, y: self.bounds.height - 5), radius: 5, startAngle: 2 * .pi, endAngle:.pi / 2, clockwise: true)
        path.addLine(to: CGPoint(x: 0, y: self.bounds.height))
        path.addLine(to: CGPoint(x: 15, y: self.bounds.height / 2))
        path.close()
    }
    
    // Update the reusable layer's content
    btnLayer.path = path.cgPath
    btnLayer.fillColor = self.buttonColor.cgColor
    btnLayer.frame = self.bounds // Match button's current bounds
    
    // Adjust title insets
    if thirdStep || secondStep{
        self.titleEdgeInsets = UIEdgeInsets(top: 0, left: 25, bottom: 0, right: 0)
    }else{
        self.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)
    }
}

Step 3: Trigger Refresh When Bounds Change (Rotation)

To make sure the button redraws when the device rotates (and its bounds change), add an override for the bounds property or use layoutSubviews() to call setNeedsDisplay():

Option 1: Override bounds

override var bounds: CGRect {
    didSet {
        setNeedsDisplay()
    }
}

Option 2: Use layoutSubviews()

override func layoutSubviews() {
    super.layoutSubviews()
    setNeedsDisplay()
}

Bonus: Refresh When Step State Changes

Notice I added setNeedsDisplay() to each of the step property's didSet blocks. This ensures the button redraws immediately when you toggle between first/second/third step states in code or via Interface Builder.

With these changes, your button will:

  • Only use one shape layer at a time (no more overlapping)
  • Automatically refresh its shape when the device rotates
  • Update instantly when you change its step state

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 19:22:59