设备方向改变时CoreGraphics自定义按钮重绘异常求助
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

