Search code examples
iosiphoneobjective-cuiviewuibezierpath

Drawing UIBezierPath on code generated UIView


I have a UIView added in code at run time.

I want to draw a UIBezierPath in it, but does this means i have to override the drawRect for UIView?

Or is there another way of drawing to it on the custom made UIView?

Here is the code for generating the UIView:

UIView* shapeView = [[UIView alloc]initWithFrame:CGRectMake(xOrigin,yOrigin+(i*MENU_BLOCK_FRAME_HEIGHT), self.shapeScroll.frame.size.width, MENU_BLOCK_FRAME_HEIGHT)];
shapeView.clipsToBounds = YES;

And here is the function to create and return a UIBezierPath:

- (UIBezierPath*)createPath
{
    UIBezierPath* path = [[UIBezierPath alloc]init];
    [path moveToPoint:CGPointMake(100.0, 50.0)];
    [path addLineToPoint:CGPointMake(200.0,50.0)];
    [path addLineToPoint:CGPointMake(200.0, 200.0)];
    [path addLineToPoint:CGPointMake(100.0, 200.0)];
    [path closePath];
    return path;
}

Solution

  • It wasn't long ago that I didn't even know how to pronounce Bézier, let alone know how to use Bézier paths to make a custom shape. The following is what I have learned. It turns out that they aren't as scary as they seem at first.

    How to draw a Bézier path in a custom view

    These are the main steps:

    1. Design the outline of the shape you want.
    2. Divide the outline path into segments of lines, arcs, and curves.
    3. Build that path programmatically.
    4. Draw the path either in drawRect or using a CAShapeLayer.

    Design shape outline

    You could do anything, but as an example I have chosen the shape below. It could be a popup key on a keyboard.

    enter image description here

    Divide the path into segments

    Look back at your shape design and break it down into simpler elements of lines (for straight lines), arcs (for circles and round corners), and curves (for anything else).

    Here is what our example design would look like:

    enter image description here

    • Black are line segments
    • Light blue are arc segments
    • Red are curves
    • Orange dots are the control points for the curves
    • Green dots are the points between path segments
    • Dotted lines show the bounding rectangle
    • Dark blue numbers are the segments in the order that they will be added programmatically

    Build the path programmatically

    We'll arbitrarily start in the bottom left corner and work clockwise. I'll use the grid in the image to get the x and y values for the points. I'll hardcode everything here, but of course you wouldn't do that in a real project.

    The basic process is:

    1. Create a new UIBezierPath
    2. Choose a starting point on the path with moveToPoint
    3. Add segments to the path
      • line: addLineToPoint
      • arc: addArcWithCenter
      • curve: addCurveToPoint
    4. Close the path with closePath

    Here is the code to make the path in the image above.

    func createBezierPath() -> UIBezierPath {
    
        // create a new path
        let path = UIBezierPath()
    
        // starting point for the path (bottom left)
        path.move(to: CGPoint(x: 2, y: 26))
    
        // *********************
        // ***** Left side *****
        // *********************
    
        // segment 1: line
        path.addLine(to: CGPoint(x: 2, y: 15))
    
        // segment 2: curve
        path.addCurve(to: CGPoint(x: 0, y: 12), // ending point
            controlPoint1: CGPoint(x: 2, y: 14),
            controlPoint2: CGPoint(x: 0, y: 14))
    
        // segment 3: line
        path.addLine(to: CGPoint(x: 0, y: 2))
    
        // *********************
        // ****** Top side *****
        // *********************
    
        // segment 4: arc
        path.addArc(withCenter: CGPoint(x: 2, y: 2), // center point of circle
            radius: 2, // this will make it meet our path line
            startAngle: CGFloat(M_PI), // π radians = 180 degrees = straight left
            endAngle: CGFloat(3*M_PI_2), // 3π/2 radians = 270 degrees = straight up
            clockwise: true) // startAngle to endAngle goes in a clockwise direction
    
        // segment 5: line
        path.addLine(to: CGPoint(x: 8, y: 0))
    
        // segment 6: arc
        path.addArc(withCenter: CGPoint(x: 8, y: 2),
                              radius: 2,
                              startAngle: CGFloat(3*M_PI_2), // straight up
            endAngle: CGFloat(0), // 0 radians = straight right
            clockwise: true)
    
        // *********************
        // ***** Right side ****
        // *********************
    
        // segment 7: line
        path.addLine(to: CGPoint(x: 10, y: 12))
    
        // segment 8: curve
        path.addCurve(to: CGPoint(x: 8, y: 15), // ending point
            controlPoint1: CGPoint(x: 10, y: 14),
            controlPoint2: CGPoint(x: 8, y: 14))
    
        // segment 9: line
        path.addLine(to: CGPoint(x: 8, y: 26))
    
        // *********************
        // **** Bottom side ****
        // *********************
    
        // segment 10: line
        path.close() // draws the final line to close the path
    
        return path
    }
    

    Note: Some of the above code can be reduced by adding a line and an arc in a single command (since the arc has an implied starting point). See here for more details.

    Draw the path

    We can draw the path either in a layer or in drawRect.

    Method 1: Draw path in a layer

    Our custom class looks like this. We add our Bezier path to a new CAShapeLayer when the view is initialized.

    import UIKit
    class MyCustomView: UIView {
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setup()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setup()
        }
    
        func setup() {
    
            // Create a CAShapeLayer
            let shapeLayer = CAShapeLayer()
    
            // The Bezier path that we made needs to be converted to 
            // a CGPath before it can be used on a layer.
            shapeLayer.path = createBezierPath().cgPath
    
            // apply other properties related to the path
            shapeLayer.strokeColor = UIColor.blue.cgColor
            shapeLayer.fillColor = UIColor.white.cgColor
            shapeLayer.lineWidth = 1.0
            shapeLayer.position = CGPoint(x: 10, y: 10)
    
            // add the new layer to our custom view
            self.layer.addSublayer(shapeLayer)
        }
    
        func createBezierPath() -> UIBezierPath {
    
            // see previous code for creating the Bezier path
        }
    }
    

    And creating our view in the View Controller like this

    override func viewDidLoad() {
        super.viewDidLoad()
    
        // create a new UIView and add it to the view controller
        let myView = MyCustomView()
        myView.frame = CGRect(x: 100, y: 100, width: 50, height: 50)
        myView.backgroundColor = UIColor.yellow
        view.addSubview(myView)
    
    }
    

    We get...

    enter image description here

    Hmm, that's a little small because I hardcoded all the numbers in. I can scale the path size up, though, like this:

    let path = createBezierPath()
    let scale = CGAffineTransform(scaleX: 2, y: 2)
    path.apply(scale)
    shapeLayer.path = path.cgPath
    

    enter image description here

    Method 2: Draw path in draw

    Using draw is slower than drawing to the layer, so this is not the recommended method if you don't need it.

    Here is the revised code for our custom view:

    import UIKit
    class MyCustomView: UIView {
    
        override func draw(_ rect: CGRect) {
    
            // create path (see previous code)
            let path = createBezierPath()
    
            // fill
            let fillColor = UIColor.white
            fillColor.setFill()
    
            // stroke
            path.lineWidth = 1.0
            let strokeColor = UIColor.blue
            strokeColor.setStroke()
    
            // Move the path to a new location
            path.apply(CGAffineTransform(translationX: 10, y: 10))
    
            // fill and stroke the path (always do these last)
            path.fill()
            path.stroke()
    
        }
    
        func createBezierPath() -> UIBezierPath {
    
            // see previous code for creating the Bezier path
        }
    }
    

    which gives us the same result...

    enter image description here

    Further study

    I really recommend looking at the following materials. They are what finally made Bézier paths understandable for me. (And taught me how to pronounce it: /ˈbɛ zi eɪ/.)