Search code examples
iosswiftuibuttonuibezierpath

UIBezierPath rounded corners in UIButton


I want to draw a tappable bubble shape around text. In order to do that I decided to add a shapelayer to UIButton like this:

// Button also has this
bubbleButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
bubbleButton.setTitle("Gutp", for: .normal)

// In my subclass of UIButton
override func layoutSubviews() {
    super.layoutSubviews()

    self.bubbleLayer?.removeFromSuperlayer()

    let maskLayer = CAShapeLayer.init()
    let bezierPath = UIBezierPath.init(roundedRect: self.bounds,
                                       byRoundingCorners: [.topRight, .topLeft, .bottomLeft],
                                       cornerRadii: CGSize(width: 20, height: 20))
    maskLayer.path = bezierPath.cgPath
    maskLayer.strokeColor = UIColor.red.cgColor
    maskLayer.lineWidth = 2
    maskLayer.fillColor = UIColor.clear.cgColor
    maskLayer.backgroundColor = UIColor.clear.cgColor
    maskLayer.isOpaque = false

    self.bubbleLayer = maskLayer

    if let layers = self.layer.sublayers {
        self.layer.insertSublayer(self.bubbleLayer!, at: UInt32(layers.count))
    } else {
        self.layer.addSublayer(self.bubbleLayer!)
    }
}

Please don't look at the performance at the moment.

I have 2 buttons like this added into a UIStackView.

I get an interesting artefact (a tail if one can say so) in some cases of text (usually when it is short) and a normal bubble in case of longer text:bezierpath-artefact

How can I fix this? And why do I get such behavior?

EDIT: Linking other possibly related questions about broken cornerRadii parameter in bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:. Maybe it will help someone with similar issues.

  1. Why is cornerRadii parameter of CGSize type in -[UIBezierPath bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:]?

  2. Crazy rounded rect UIBezierPath behavior on iOS 7. What is the deal?


Solution

  • For using these rounded rect methods you must ensure that the view size is larger then the radius used. In your case both width and height must be larger then 40 (Radii size is 20x20 so max(20*2, 20*2) = 40).

    In general I prefer having a custom method to generate such paths. Using lines and arcs usually give you better flexibility. You may try the following:

        /// Returns a path with rounded corners
        ///
        /// - Parameters:
        ///   - frame: A frame at which the path is drawn. To fit in view "bounds" should be used
        ///   - maximumRadius: A maximum corner radius used. For smaller views radius will be min(width/2, height/2)
        /// - Returns: Returns a new path
        func roundedRectPath(inRect frame: CGRect, radiusConstrainedTo maximumRadius: CGFloat) -> UIBezierPath {
    
            let radisu = min(maximumRadius, min(frame.size.width*0.5, frame.size.height*0.5))
    
            let path = UIBezierPath()
    
            path.move(to: CGPoint(x: frame.origin.x + radisu, y: frame.origin.y)) // Top left
            path.addLine(to: CGPoint(x: frame.origin.x + frame.size.width - radisu, y: frame.origin.y))  // Top right
            path.addQuadCurve(to: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y + frame.size.height - radisu), controlPoint: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y)) // Top right arc
            path.addLine(to: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y + frame.size.height - radisu))  // Bottom right
            path.addQuadCurve(to: CGPoint(x: frame.origin.x + frame.size.width - radisu, y: frame.origin.y + frame.size.height), controlPoint: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y + frame.size.height)) // Bottom right arc
            path.addLine(to: CGPoint(x: frame.origin.x + radisu, y: frame.origin.y + frame.size.height))  // Bottom left
            path.addQuadCurve(to: CGPoint(x: frame.origin.x, y: frame.origin.y + frame.size.height - radisu), controlPoint: CGPoint(x: frame.origin.x, y: frame.origin.y + frame.size.height)) // Bottom left arc
            path.addLine(to: CGPoint(x: frame.origin.x, y: frame.origin.y + radisu))  // Top left
            path.addQuadCurve(to: CGPoint(x: frame.origin.x + radisu, y: frame.origin.y), controlPoint: CGPoint(x: frame.origin.x, y: frame.origin.y)) // Top left arc
            path.close()
    
            return path
        }
    

    When using this with stroke you need to also inset the frame by half of the line width. This is a snippet from "draw rect" procedure but can be applied anywhere:

            UIColor.red.setStroke()
            let lineWidth: CGFloat = 5.0
            let path = roundedRectPath(inRect: bounds.insetBy(dx: lineWidth*0.5, dy: lineWidth*0.5), radiusConstrainedTo: 30.0)
            path.lineWidth = lineWidth
            path.stroke()
    

    Notice the bounds.insetBy(dx: lineWidth*0.5, dy: lineWidth*0.5).