圆形边框不 clipping/masking 完美

Round borders aren't clipping/masking perfectly

我在 Interface Builder 中制作了一个标签,具有固定高度和固定宽度的约束:

我将其子类化为白色圆形边框:

class CircularLabel: UILabel {
    override func awakeFromNib() {
        super.awakeFromNib()
        layer.cornerRadius = frame.size.height / 2
        layer.borderColor = UIColor.white.cgColor
        layer.borderWidth = 5
        layer.masksToBounds = true
        clipsToBounds = true
    }
}

但是 clipping/masking 在运行时并不好:

我期待完美的白色边框,没有橙色像素。

iPhone8(模拟器和真机),iOS11.2,Xcode9.2,Swift3.2

MCVE 在 https://github.com/Coeur/Whosebug48658502

您应该使用 UIBezierPath 来圆角并使用相同的路径绘制边界线。 我的情况是我创建了 CAShapeLayer 并进行了所有调整并将其添加为要查看的子层。


let borderLayer = CAShapeLayer()
let shapeLayer = CAShapeLayer()

let path = UIBezierPath(roundedRect: *get your view bounds*, cornerRadius: *needed radius*).cgPath

//Set this rounding path to both layers
shapeLayer.path = path
borderLayer.path = path

//adjust border layer
borderLayer.lineWidth = *border width*
borderLayer.strokeColor = *cgColor of your border*

//apply shape layer as mask to your view, it will cut your view by the corners
*yourViewInstance*.layer.mask = shapeLayer

//Set fill color for border layer as clear, because technically it just puts colored layer over your view
borderLayer.fillColor = UIColor.clear.cgColor

//Add border layer as sublayer to your view's main layer
*your view instance*.layer.addSublayer(borderLayer)

在您的情况下,动态标签的文本可能存在问题:如果文本是例如900000 它将绘制在边框下。要解决这个问题,您可以将 UILAbel 放在另一个视图(将包含形状和边框调整)中并对其进行布局。
示例:
Structure and constraints

What i got: container BG - orange, border - white, superview's BG - red

Controller 的 viewDidLoad 方法代码:

override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = UIColor.red
        self.containerView.backgroundColor = UIColor.orange

        self.label.backgroundColor = UIColor.clear
        self.label.textAlignment = .center
        self.label.adjustsFontSizeToFitWidth = true
        self.label.text = "9000000"

        //Create Border and shape and apply it to container view

        let borderLayer = CAShapeLayer()
        let shapeLayer = CAShapeLayer()

        let path = UIBezierPath(roundedRect: containerView.bounds, cornerRadius: containerView.bounds.width / 2).cgPath

        //Set this rounding path to both layers
        shapeLayer.path = path
        borderLayer.path = path

        //adjust border layer
        borderLayer.lineWidth = 20
        borderLayer.strokeColor = UIColor.white.cgColor

        //apply shape layer as mask to your view, it will cut your view by the corners
        self.containerView.layer.mask = shapeLayer

        //Set fill color for border layer as clear, because technically it just puts colored layer over your view
        borderLayer.fillColor = UIColor.clear.cgColor

        //Add border layer as sublayer to your view's main layer
        self.containerView.layer.addSublayer(borderLayer)
    }

另一个解决方案是完全忘记不完美的 borderWidth 并在彼此内部使用 两个视图

extension UIView {
    func roundBounds() {
        layer.cornerRadius = frame.size.height / 2
        clipsToBounds = true
    }
}

class RoundLabel: UILabel {
    override func awakeFromNib() {
        super.awakeFromNib()
        roundBounds()
    }
}

class RoundView: UIView {
    override func awakeFromNib() {
        super.awakeFromNib()
        roundBounds()
    }
}

谜底已解。

很好的解决方案

添加一个 1 像素的描边,masksToBounds 将完成正确剪裁边缘的工作:

override func draw(_ rect: CGRect) {
    super.draw(rect)
    // workaround incomplete borders: 
    UIColor(cgColor: layer.borderColor!).setStroke()
    let path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius)
    path.lineWidth = 1
    path.stroke()
}

解释

实际上,根据我的测试,设置 layer.borderWidth = 5 等同于公式:

let borderWidth: CGFloat = 5
UIColor(cgColor: layer.borderColor!).setStroke()
let path = UIBezierPath(roundedRect: bounds.insetBy(dx: borderWidth / 2, dy: borderWidth / 2),
                        cornerRadius: layer.cornerRadius - borderWidth / 2)
path.lineWidth = borderWidth
path.stroke()

但另一方面,layer.cornerRadius = frame.size.height / 2 + layer.masksToBounds = true 将使用不同的未知方法进行裁剪,该方法在边缘具有不同的别名公式。因为裁剪和绘图没有相同的别名,所以有一些像素显示背景颜色而不是边框​​颜色。