UIBezierPath: How to add a border around a view with rounded corners?

前端 未结 4 1382
故里飘歌
故里飘歌 2020-12-01 14:31

I am using UIBezierPath to have my imageview have round corners but I also want to add a border to the imageview. Keep in mind the top is a uiimage and the bottom is a label

4条回答
  •  星月不相逢
    2020-12-01 14:54

    Absolutely perfect 2019 solution

    Without further ado, here's exactly how you do this.

    1. Don't actually use the "basic" layer that comes with the view
    2. Make a new layer only for the image. You can now mask this (circularly) without affecting the next layer
    3. Make a new layer for the border as such. It will safely not be masked by the picture layer.

    The key facts are

    1. With a CALayer, you can indeed apply a .mask and it only affects that layer
    2. When drawing a circle (or indeed any border), have to attend very carefully to the fact that you only get "half the width" - in short never crop using the same path you draw with.
    3. Notice the original cat image is exactly as wide as the horizontal yellow arrow. You have to be careful to paint the image so that the whole image appears in the roundel, which is smaller than the overall custom control.

    So, setup in the usual way

    import UIKit
    
    @IBDesignable class GreenCirclePerson: UIView {
        
        @IBInspectable var borderColor: UIColor = UIColor.black { didSet { setup() } }
        @IBInspectable var trueBorderThickness: CGFloat = 2.0 { didSet { setup() } }
        @IBInspectable var trueGapThickness: CGFloat = 2.0 { didSet { setup() } }
        
        @IBInspectable var picture: UIImage? = nil { didSet { setup() } }
        
        override func layoutSubviews() { setup() }
        
        var imageLayer: CALayer? = nil
        var border: CAShapeLayer? = nil
        
        func setup() {
            
            if (imageLayer == nil) {
                imageLayer = CALayer()
                self.layer.addSublayer(imageLayer!)
            }
            if (border == nil) {
                border = CAShapeLayer()
                self.layer.addSublayer(border!)
            }
            
    

    Now carefully make the layer for the circularly-cropped image:

            // the ultimate size of our custom control:
            let box = self.bounds.aspectFit()
            
            let totalInsetOnAnyOneSide = trueBorderThickness + trueGapThickness
            
            let boxInWhichImageSits = box.inset(by:
               UIEdgeInsets(top: totalInsetOnAnyOneSide, left: totalInsetOnAnyOneSide,
               bottom: totalInsetOnAnyOneSide, right: totalInsetOnAnyOneSide))
            
            // just a note. that version of inset#by is much clearer than the
            // confusing dx/dy variant, so best to use that one
            
            imageLayer!.frame = boxInWhichImageSits
            imageLayer!.contents = picture?.cgImage
            imageLayer?.contentsGravity = .resizeAspectFill
            
            let halfImageSize = boxInWhichImageSits.width / 2.0
            
            let maskPath = UIBezierPath(roundedRect: imageLayer!.bounds,
               cornerRadius:halfImageSize)
            let maskLayer = CAShapeLayer()
            maskLayer.path = maskPath.cgPath
            imageLayer!.mask = maskLayer
            
    

    Next as a completely separate layer, draw the border as you wish:

            // now create the border
            
            border!.frame = bounds
            
            // To draw the border, you must inset it by half the width of the border,
            // otherwise you'll be drawing only half the border. (Indeed, as an additional
            // subtle problem you are clipping rather than rendering the outside edge.)
            
            let halfWidth = trueBorderThickness / 2.0
            let borderCenterlineBox = box.inset(by:
                UIEdgeInsets(top: halfWidth, left: halfWidth,
                bottom: halfWidth, right: halfWidth))
            
            let halfBorderBoxSize = borderCenterlineBox.width / 2.0
            
            let borderPath = UIBezierPath(roundedRect: borderCenterlineBox,
              cornerRadius:halfBorderBoxSize)
            
            border!.path = borderPath.cgPath
            border!.fillColor = UIColor.clear.cgColor
            
            border!.strokeColor = borderColor.cgColor
            border!.lineWidth = trueBorderThickness
        }
    }
    

    Everything works perfectly as in iOS standard controls:

    Everything which is invisible is invisible; you can see-through the overall custom control to any material behind, there are no "half thickness" problems or missing image material, you can set the custom control background color in the usual way, etc etc. The inspector controls all work properly. (Phew!)

    Similar solutions:

    https://stackoverflow.com/a/57465440/294884 - image + rounded + shadows
    https://stackoverflow.com/a/41553784/294884 - two-corner problem
    https://stackoverflow.com/a/59092828/294884 - "shadows + hole" or "glowbox" problem
    https://stackoverflow.com/a/57400842/294884 - the "border AND gap" problem
    https://stackoverflow.com/a/57514286/294884 - basic "adding" beziers

提交回复
热议问题