ioscocoacocoa-touchcore-graphicscgpath

iOS UIView - Different rounding radius for each of the 4 corners


How can I set a completely different rounding radius for each of the 4 corners of a UIView?

UIBezierPath allows me to set ONE value for one or more specific corners, but not a different value for each corner.

I think in theory it should be possible with a custom CGPath but I'm unable to implement it.


Solution

  • Give this a try - you can paste it directly into a Playground page and see how it works:

    import UIKit
    import PlaygroundSupport
    
    extension Int {
        var degreesToRadians: Double { return Double(self) * .pi / 180 }
    }
    extension FloatingPoint {
        var degreesToRadians: Self { return self * .pi / 180 }
        var radiansToDegrees: Self { return self * 180 / .pi }
    }
    
    class VariableCornerRadiusView: UIView  {
    
        var upperLeftCornerRadius:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var upperRightCornerRadius:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var lowerLeftCornerRadius:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var lowerRightCornerRadius:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
    
        func layoutMask() -> Void {
    
            var pt = CGPoint.zero
    
            let myBezier = UIBezierPath()
    
            pt.x = upperLeftCornerRadius
            pt.y = 0
    
            myBezier.move(to: pt)
    
            pt.x = bounds.width - upperRightCornerRadius
            pt.y = 0
    
            myBezier.addLine(to: pt)
    
            pt.x = bounds.width - upperRightCornerRadius
            pt.y = upperRightCornerRadius
    
            myBezier.addArc(withCenter: pt, radius: upperRightCornerRadius, startAngle: CGFloat(270.degreesToRadians), endAngle: CGFloat(0.degreesToRadians), clockwise: true)
    
            pt.x = bounds.width
            pt.y = bounds.height - lowerRightCornerRadius
    
            myBezier.addLine(to: pt)
    
            pt.x = bounds.width - lowerRightCornerRadius
            pt.y = bounds.height - lowerRightCornerRadius
    
            myBezier.addArc(withCenter: pt, radius: lowerRightCornerRadius, startAngle: CGFloat(0.degreesToRadians), endAngle: CGFloat(90.degreesToRadians), clockwise: true)
    
            pt.x = lowerLeftCornerRadius
            pt.y = bounds.height
    
            myBezier.addLine(to: pt)
    
            pt.x = lowerLeftCornerRadius
            pt.y = bounds.height - lowerLeftCornerRadius
    
            myBezier.addArc(withCenter: pt, radius: lowerLeftCornerRadius, startAngle: CGFloat(90.degreesToRadians), endAngle: CGFloat(180.degreesToRadians), clockwise: true)
    
            pt.x = 0
            pt.y = upperLeftCornerRadius
    
            myBezier.addLine(to: pt)
    
            pt.x = upperLeftCornerRadius
            pt.y = upperLeftCornerRadius
    
            myBezier.addArc(withCenter: pt, radius: upperLeftCornerRadius, startAngle: CGFloat(180.degreesToRadians), endAngle: CGFloat(270.degreesToRadians), clockwise: true)
    
            myBezier.close()
    
            let maskForPath = CAShapeLayer()
            maskForPath.path = myBezier.cgPath
            layer.mask = maskForPath
    
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            self.layoutMask()
        }
    
    }
    
    var testSize = CGSize(width: 200, height: 200)
    
    // set up an orange view to hold it...
    let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 260))
    
    containerView.backgroundColor = UIColor.orange
    
    // create a VariableCornerRadiusView, just a little smaller than the container view
    let TestView = VariableCornerRadiusView(frame: containerView.bounds.insetBy(dx: 20, dy: 20))
    
    // set different radius for each corner
    TestView.upperLeftCornerRadius = 20.0
    TestView.upperRightCornerRadius = 40.0
    TestView.lowerRightCornerRadius = 60.0
    TestView.lowerLeftCornerRadius = 80.0
    
    // give it a blue background
    TestView.backgroundColor = UIColor.blue
    
    // add it to the container
    containerView.addSubview(TestView)
    
    // show it
    PlaygroundPage.current.liveView = containerView
    

    Result should look like this:

    enter image description here


    Edit another approach... Using a filled shape layer (instead of a mask) and a shadow:

    import UIKit
    import PlaygroundSupport
    
    extension Int {
        var degreesToRadians: Double { return Double(self) * .pi / 180 }
    }
    extension FloatingPoint {
        var degreesToRadians: Self { return self * .pi / 180 }
        var radiansToDegrees: Self { return self * 180 / .pi }
    }
    
    class VariableCornerRadiusShadowView: UIView  {
    
        var upperLeftCornerRadius:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var upperRightCornerRadius:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var lowerLeftCornerRadius:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var lowerRightCornerRadius:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var fillColor: UIColor = .white {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var shadowColor: UIColor = .black {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var shadowOffset: CGSize = CGSize(width: 0.0, height: 2.0) {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var shadowOpacity: Float = 0.5 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        var shadowRadius: CGFloat = 8.0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        let theShapeLayer = CAShapeLayer()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
    
        func commonInit() -> Void {
            layer.addSublayer(theShapeLayer)
        }
    
        func layoutShape() -> Void {
    
            var pt = CGPoint.zero
    
            let myBezier = UIBezierPath()
    
            pt.x = upperLeftCornerRadius
            pt.y = 0
    
            myBezier.move(to: pt)
    
            pt.x = bounds.width - upperRightCornerRadius
            pt.y = 0
    
            myBezier.addLine(to: pt)
    
            pt.x = bounds.width - upperRightCornerRadius
            pt.y = upperRightCornerRadius
    
            myBezier.addArc(withCenter: pt, radius: upperRightCornerRadius, startAngle: CGFloat(270.degreesToRadians), endAngle: CGFloat(0.degreesToRadians), clockwise: true)
    
            pt.x = bounds.width
            pt.y = bounds.height - lowerRightCornerRadius
    
            myBezier.addLine(to: pt)
    
            pt.x = bounds.width - lowerRightCornerRadius
            pt.y = bounds.height - lowerRightCornerRadius
    
            myBezier.addArc(withCenter: pt, radius: lowerRightCornerRadius, startAngle: CGFloat(0.degreesToRadians), endAngle: CGFloat(90.degreesToRadians), clockwise: true)
    
            pt.x = lowerLeftCornerRadius
            pt.y = bounds.height
    
            myBezier.addLine(to: pt)
    
            pt.x = lowerLeftCornerRadius
            pt.y = bounds.height - lowerLeftCornerRadius
    
            myBezier.addArc(withCenter: pt, radius: lowerLeftCornerRadius, startAngle: CGFloat(90.degreesToRadians), endAngle: CGFloat(180.degreesToRadians), clockwise: true)
    
            pt.x = 0
            pt.y = upperLeftCornerRadius
    
            myBezier.addLine(to: pt)
    
            pt.x = upperLeftCornerRadius
            pt.y = upperLeftCornerRadius
    
            myBezier.addArc(withCenter: pt, radius: upperLeftCornerRadius, startAngle: CGFloat(180.degreesToRadians), endAngle: CGFloat(270.degreesToRadians), clockwise: true)
    
            myBezier.close()
    
            theShapeLayer.path = myBezier.cgPath
            theShapeLayer.fillColor = fillColor.cgColor
    
            layer.shadowRadius = shadowRadius
            layer.shadowOffset = shadowOffset
            layer.shadowOpacity = shadowOpacity
            layer.shadowColor = shadowColor.cgColor
    
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            self.layoutShape()
        }
    
    }
    
    var testSize = CGSize(width: 200, height: 200)
    
    // set up an orange "container" view to hold it...
    let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 260))
    
    containerView.backgroundColor = UIColor.white
    
    let sampleView = VariableCornerRadiusShadowView(frame: containerView.bounds.insetBy(dx: 20, dy: 20))
    
    // set different radius for each corner
    sampleView.upperLeftCornerRadius = 20.0
    sampleView.upperRightCornerRadius = 40.0
    sampleView.lowerRightCornerRadius = 60.0
    sampleView.lowerLeftCornerRadius = 80.0
    
    // if we want to adjust defaults
    //sampleView.fillColor = .green
    //sampleView.shadowOffset = CGSize(width: 2, height: 4)
    //sampleView.shadowRadius = 4  // not quite so "fuzzy"
    //sampleView.shadowOpacity = 0.8
    
    // add view to container
    containerView.addSubview(sampleView)
    
    // show it
    PlaygroundPage.current.liveView = containerView
    

    Result with "default" properties:

    enter image description here

    Result with properties changed to:

    .fillColor = .green
    .shadowOffset = CGSize(width: 2, height: 4)
    .shadowRadius = 4  // not quite so "fuzzy"
    .shadowOpacity = 0.8
    

    enter image description here


    Edit 2 -

    This version is now @IBDesignable, with some properties renamed to show better in IB. Also added border width and color:

    @IBDesignable
    class VariableCornerRadiusShadowView: UIView  {
    
        @IBInspectable
        var radTopLeft:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var radTopRright:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var radBotLeft:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var radBotRright:CGFloat = 0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var fillColor: UIColor = .white {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var borderColor: UIColor = .clear {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var borderWidth: CGFloat = 0.0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var shadowColor: UIColor = .black {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var shadowXOffset: CGFloat = 0.0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var shadowYOffset: CGFloat = 0.0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var shadowOpacity: Float = 0.5 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        @IBInspectable
        var shadowRadius: CGFloat = 8.0 {
            didSet {
                self.setNeedsLayout()
            }
        }
    
        let theShapeLayer = CAShapeLayer()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        override func prepareForInterfaceBuilder() {
            commonInit()
        }
    
        func commonInit() -> Void {
            backgroundColor = .clear
            layer.addSublayer(theShapeLayer)
        }
    
        func layoutShape() -> Void {
    
            var pt = CGPoint.zero
    
            let myBezier = UIBezierPath()
    
            pt.x = radTopLeft
            pt.y = 0
    
            myBezier.move(to: pt)
    
            pt.x = bounds.width - radTopRright
            pt.y = 0
    
            myBezier.addLine(to: pt)
    
            pt.x = bounds.width - radTopRright
            pt.y = radTopRright
    
            myBezier.addArc(withCenter: pt, radius: radTopRright, startAngle: .pi * 1.5, endAngle: 0, clockwise: true)
    
            pt.x = bounds.width
            pt.y = bounds.height - radBotRright
    
            myBezier.addLine(to: pt)
    
            pt.x = bounds.width - radBotRright
            pt.y = bounds.height - radBotRright
    
            myBezier.addArc(withCenter: pt, radius: radBotRright, startAngle: 0, endAngle: .pi * 0.5, clockwise: true)
    
            pt.x = radBotLeft
            pt.y = bounds.height
    
            myBezier.addLine(to: pt)
    
            pt.x = radBotLeft
            pt.y = bounds.height - radBotLeft
    
            myBezier.addArc(withCenter: pt, radius: radBotLeft, startAngle: .pi * 0.5, endAngle: .pi, clockwise: true)
    
            pt.x = 0
            pt.y = radTopLeft
    
            myBezier.addLine(to: pt)
    
            pt.x = radTopLeft
            pt.y = radTopLeft
    
            myBezier.addArc(withCenter: pt, radius: radTopLeft, startAngle: .pi, endAngle: .pi * 1.5, clockwise: true)
    
            myBezier.close()
    
            theShapeLayer.path = myBezier.cgPath
            theShapeLayer.fillColor = fillColor.cgColor
    
            theShapeLayer.strokeColor = borderColor.cgColor
            theShapeLayer.lineWidth = borderWidth
    
            layer.shadowRadius = shadowRadius
            layer.shadowOffset = CGSize(width: shadowXOffset, height: shadowYOffset)
            layer.shadowOpacity = shadowOpacity
            layer.shadowColor = shadowColor.cgColor
    
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            self.layoutShape()
        }
    
    }