iur
iur

Reputation: 2096

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?

Upvotes: 1

Views: 908

Answers (1)

Matic Oblak
Matic Oblak

Reputation: 16774

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).

Upvotes: 5

Related Questions