Search code examples
iosuikitcalayerround-rect

UIBezierPath bezierPathWithRoundedRect: the cornerRadius value is not consistent


I want to draw a round rect in an CALayer using [UIBezierPath bezierPathWithRoundedRect: cornerRadius:], when setting up the rect's cornerRadius, I found that it is not consistent from 0 to maximum radius(about half of the layer's bound). The value jumped at about 1/3 of maximum radius which is quite confusing. After some research, I found probably it's a bug of iOS7 style of UIBezierPath drawing a round rect. PaintCode's research on that. So my question is how to draw an old style of perfect round rect with consistent cornerRadius value change?


Solution

  • There's clearly an Apple bug here. When we draw a shape layer with this path:

    UIBezierPath(roundedRect: CGRect(x: 50, y: 20, width: 208, height: 50), cornerRadius: 20).cgPath
    

    This is what we get:

    enter image description here

    That's just not right. The height is 50, the corner radius is 20, so there should be an obvious flat part at the end, 10 points in height. Where did it go? Instead, Apple has increased the radius from the radius you asked for, and has just round-capped the entire ends of the rectangle. It ought to look more like this:

    enter image description here

    That's what both DonMag and I are trying to fix.

    It will be useful to have on hand a replacement rounded rect method on UIBezierBath that works as expected. We can write DonMag's code much more neatly by calling addArc(tangent1End:...); most people have never heard of this wonderful method, but it was made for exactly this sort of job. I'll also take the opportunity to establish some nice names for the corners of a CGRect:

    extension CGRect {
        var topRight: CGPoint { CGPoint(x: maxX, y: minY) }
        var topLeft: CGPoint { CGPoint(x: minX, y: minY) }
        var bottomRight: CGPoint { CGPoint(x: maxX, y: maxY) }
        var bottomLeft: CGPoint { CGPoint(x: minX, y: maxY) }
    }
    
    extension UIBezierPath {
        static func roundedRect(rect: CGRect, cornerRadius: CGFloat) -> UIBezierPath {
            let path = CGMutablePath()
            let start = CGPoint(x: rect.midX, y: rect.minY)
            path.move(to: start)
            path.addArc(tangent1End: rect.topRight, tangent2End: rect.bottomRight, radius: cornerRadius)
            path.addArc(tangent1End: rect.bottomRight, tangent2End: rect.bottomLeft, radius: cornerRadius)
            path.addArc(tangent1End: rect.bottomLeft, tangent2End: rect.topLeft, radius: cornerRadius)
            path.addArc(tangent1End: rect.topLeft, tangent2End: start, radius: cornerRadius)
            return UIBezierPath(cgPath: path)
        }
    }
    

    Now simply call UIBezierPath.roundedRect(rect:cornerRadius) instead of the buggy UIBezierPath(roundedRect:cornerRadius), and the problem is solved.

    If you want to go further, we can include a parameter allowing us to specify what corners to round:

    extension UIBezierPath {
        static func roundedRect(
            rect: CGRect,
            corners: UIRectCorner = .allCorners,
            cornerRadius: CGFloat
        ) -> UIBezierPath {
            let path = CGMutablePath()
            let start = CGPoint(x: rect.midX, y: rect.minY)
            path.move(to: start)
            if corners.contains(.topRight) {
                path.addArc(tangent1End: rect.topRight, tangent2End: rect.bottomRight, radius: cornerRadius)
            } else {
                path.addLine(to: rect.topRight)
            }
            if corners.contains(.bottomRight) {
                path.addArc(tangent1End: rect.bottomRight, tangent2End: rect.bottomLeft, radius: cornerRadius)
            } else {
                path.addLine(to: rect.bottomRight)
            }
            if corners.contains(.bottomLeft) {
                path.addArc(tangent1End: rect.bottomLeft, tangent2End: rect.topLeft, radius: cornerRadius)
            } else {
                path.addLine(to: rect.bottomLeft)
            }
            if corners.contains(.topLeft) {
                path.addArc(tangent1End: rect.topLeft, tangent2End: start, radius: cornerRadius)
            } else {
                path.addLine(to: rect.topLeft)
            }
            return UIBezierPath(cgPath: path)
        }
    }
    

    EDIT: Okay, but it doesn't look right! That's because I'm drawing with the .circular style of rounded corner, but Apple draws with the .continuous style. Let's fix that by emulating the .continuous style:

    
    extension CGRect {
        var topRight: CGPoint { CGPoint(x: maxX, y: minY) }
        var topLeft: CGPoint { CGPoint(x: minX, y: minY) }
        var bottomRight: CGPoint { CGPoint(x: maxX, y: maxY) }
        var bottomLeft: CGPoint { CGPoint(x: minX, y: maxY) }
    }
    
    extension CGPoint {
        func offset(x xOffset: CGFloat, y yOffset: CGFloat) -> CGPoint {
            CGPoint(x: x + xOffset, y: y + yOffset)
        }
    }
    
    extension UIBezierPath {
        static func roundedRect(
            rect: CGRect,
            corners: UIRectCorner = .allCorners,
            cornerRadius: CGFloat
        ) -> UIBezierPath {
            let tweak = 1.2 // could experiment with this
            let offset = cornerRadius * tweak
            if rect.height > 2 * offset { // less than that, my algorithm starts to break down — but theirs works!
                let path = CGMutablePath()
                let start = CGPoint(x: rect.midX, y: rect.minY)
                path.move(to: start)
                if corners.contains(.topRight) {
                    path.addLine(to: rect.topRight.offset(x: -offset, y: 0))
                    path.addQuadCurve(to: rect.topRight.offset(x: 0, y: offset), control: rect.topRight)
                } else {
                    path.addLine(to: rect.topRight)
                }
                if corners.contains(.bottomRight) {
                    path.addLine(to: rect.bottomRight.offset(x: 0, y: -offset))
                    path.addQuadCurve(to: rect.bottomRight.offset(x: -offset, y: 0), control: rect.bottomRight)
                } else {
                    path.addLine(to: rect.bottomRight)
                }
                if corners.contains(.bottomLeft) {
                    path.addLine(to: rect.bottomLeft.offset(x: offset, y: 0))
                    path.addQuadCurve(to: rect.bottomLeft.offset(x: -0, y: -offset), control: rect.bottomLeft)
                } else {
                    path.addLine(to: rect.bottomLeft)
                }
                if corners.contains(.topLeft) {
                    path.addLine(to: rect.topLeft.offset(x: 0, y: offset))
                    path.addQuadCurve(to: rect.topLeft.offset(x: offset, y: 0), control: rect.topLeft)
                } else {
                    path.addLine(to: rect.topLeft)
                }
                return UIBezierPath(cgPath: path)
            }
            return UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
        }
    }