Bezier paths in practice (I): From basic shapes to custom designable controls - Digital Leaves
953
post-template-default,single,single-post,postid-953,single-format-standard,qode-social-login-1.0,qode-restaurant-1.0,ajax_fade,page_not_loaded,,select-theme-ver-4.1,wpb-js-composer js-comp-ver-5.2,vc_responsive

Bezier paths in practice (I): From basic shapes to custom designable controls

CoreGraphics and UIKit provide great tools for customizing the user interface of our Apps and building custom shapes, views and animations, from the lowest level APIs to the highest, more comfortable ones (like UIView’s animateWithDuration). One of the most powerful tools for building custom views, shapes and controls are probably the Bezier paths, represented by the low level CoreGraphics class CGPath and conveniently wrapped in the more interesting UIBezierPath class. Unfortunately, its interface is frequently seen as unfriendly or too technical, so many developers opt not to dive into it, resorting to the literature or copying&pasting after a quick search in order to solve a concrete purpose. This is really a shame, as a full understanding of UIBezierPath and the CoreGraphics framework allows us to achieve really cool UI setups that can make a difference in our Apps. If you find yourself in this situation, in this post I will try to share some basic info on bezier paths in iOS (and OS X) to get you started.

You can access the code for the playgrounds containing the basic shapes tutorial, the image masking tutorial and the custom view modifications tutorial. You can also grab your copy of the custom control example. (Note: these tutorials have been built in Xcode 7 beta)

Bezier paths in (brief) theory

Bézier_4_bigA bezier path is a way of describing a shape by means of a set of points and the relationships that lead from one point to the next. It originates from the concept of “Bezier curves“, developed by the french mathematician Pierre Bézier. In a simplified way, we can say that a Bezier curve is the curve contained between several points, the anchor (or coordinate) points, and the control points (that influence the shape of the curve).

There are many types of Bezier curves (linear, quadratic, cubic…), and they have been used extensively in computer graphics and image manipulation (if you’ve ever worked with shapes in Pixelmator or Photoshop you have already dealt with them) because they are specially suited for representing shapes with curved, rounded sections. If you want to know more about them, there is a really deep analysis here.

15674dd2e4b8cf7041d33b18eddec57cYou don’t need however, to know how they are built or the maths involved in the process. You can think on Bezier paths as those old drawing quizzes for children where you would join several points together to form a shape. You can fit as many points as you want in your UIBezierPath. You don’t even need to close the path.

Bezier paths can be filled, stroked or both. When you stroke a Bezier path, you create a linear path from point 1 to point n, like a children would do with the drawing at the left. When you fill it, you cover the entire surface defined by the shape with a certain color. You can customize both the stroke and fill colors, and the width of the stroked line. A Bezier path is built by selecting an initial point, moving to that point and then start adding lines or curves to new points until the las point is reached or the path is closed.

A Bezier path will, therefore, allow us to define and represent polygonal structs, from a line to a complex shape, and apply them to almost everything in UIKit thanks to the CoreGraphics framework. This is as powerful as it sounds, as it allows us to apply masks to images (to fit certain shapes), draw polygons, create shapes, divide images in expandable regions or even clip UIViews to form custom structures.

Building a UIBezierPath and creating custom shapes

Depending on the shape you want to obtain, you have several initializers for UIBezierPath:

  • UIBezierPath(): the argument-less initializer will allow you to create a custom Bezier path by adding lines and curves from point 1 to point n.
  • UIBezierPath(rect: CGRect): builds a rectangular Bezier path
  • UIBezierPath(ovalInRect: CGRect): builds a circular shape, adjusted to the rect bounds.
  • UIBezierPath(roundedRect: CGRect, cornerRadius: CGFloat): creates a rectangular shape with rounded corners (like the iOS App icons, for example)
  • UIBezierPath(arcCenter: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool): builds an arc shape with a given start and end angles and a radius, clockwise or counter-clockwise.

Most of the time, for polygons, you will start with an “empty” UIBezierPath, move to the initial point by calling moveToPoint(point: CGPoint) and then adding lines or arcs to next points using addArcWithCenter(…), addCurveToPoint(…), addLineToPoint(…), and addQuadCurveToPoint(…). Most probably you will like to close the Bezier Path between the last two points by calling closePath(). First, let’s create some basic shapes:

Next, we’ll create a more complex star shape, drawing lines point to point:

This will create the following path:

Captura de pantalla 2015-07-26 a las 20.37.55

Generating a UIImage from a UIBezierPath

One of the most simple uses of UIBezierPaths is generating (drawing) images with concrete shapes. In order to do that, we will call the CoreGraphics API to build an empty image, and draw our UIBezierPath (through its wrapped CoreGraphics CGPath) on it. CoreGraphics is the framework used for 2D drawing in Cocoa. UIKit builds on CoreGraphics and uses it to draw on its views. Every subclass of UIView has a method called drawRect(rect: CGRect) that performs the actual drawing of the view and shows it onscreen.

Even with the excellent Apple documentation on Quartz 2D and drawing using Bezier paths, Quartz and CoreGraphics can be scary at first, with all those old-school, C-like methods and function calls like CGContextConvertRectToDeviceSpace (???), but it’s easy to understand if you think about it from a painter’s perspective. Imagine that you have a blank canvas; in CoreGraphics, that’s called a “context” (represented by the CGContext object). The context is used to store the current graphical status, and its related parameters, in order to avoid sending them as parameters to all the graphics related functions. A context stores things like the current stroke and fill colors, the clipping area, the current transformation matrix, the font for drawing text, etc… Some of this parameters cannot be restored once set (like the clipping area), so in order to preserve a clean context between different drawing calls (that can alter these parameters), CoreGraphics has a stack were you can save the current graphic context by means of CGContextSaveGState, and restore it (pop from the stack) by calling CGContextRestoreGState. You should wrap your CoreGraphics calls between these two methods if your actions are going to alter the graphics context.

Every subclass of UIView has an associated CGContext, and this context is provided in drawRect so that the view can draw on it. You can also create a graphic context by means of one of the many methods from UIKit, like UIGraphicsBeginImageContext(size: CGSize) for creating a bitmap context. This is the method we will be using to create our shapes, as we will not use any subclass of UIView to render them. You can always get the current graphics context (if it exists) by calling UIGraphicsGetCurrentContext().

You draw on your canvas (context) by stroking or filling it with certain colors, and the order you paint is important (because new paint will cover your previous strokes), so you must make sure to take a bottom-to-top approach for drawing your shapes. There are many methods available for drawing:

  • CGContextAdd[Arc|LineToPoint|Lines|CurveToPoint|ArcToPoint|EllipseInRect|Rect|…]: these are the most basic drawing primitives, and their name is for the most part self-explanatory. CGContextAddLineToPoint(context, x, y) will draw a straight line in the context from the current point to the point (x, y).
  • CGContextAddPath: this is the method that interest us more, as it will allow us to draw a UIBezierPath (more concretely a CGPath obtained by calling myBezierPath.CGPath).
  • CGContextDraw[Image|TiledImage|Gradient|Shading|…]: these are more advanced primitives that will allow us to draw from gradients to images in our current context.

To change stroke and fill colors and stroke width, you can use the CGContextSetStrokeColor, CGContextSetFillColor & CGContextSetLineWidth respectively, or the builtin UIKit shortcuts on the color object: setStroke() and setFill(), applied (even though it may sound strange) directly to the UIColor instance.

So now we have all the elements we need to finish our beautiful painting. We will define a function called shapeImageWithBezierPath that will receive a Bezier path along with some drawing parameters, and will return a UIImage with the shape:

Let’s go through this code step by step:

  1. Every Bezier path has its own bounds, and its coordinate axis sits on the top-left corner. The first point of a Bezier path doesn’t necessarily has to start at point (0, 0), in fact, it could start anywhere inside its bounds. However, when we create a canvas (context), we create it with a size. If we just create a canvas with our path size and draw the path there, we could get a clipped shape if it doesn’t start on (0, 0), as the drawing primitive will start painting from the axis. That’s why it’s a good idea to “normalize” the bezier path, moving it to (0,0). This way, we can rest assured that we will draw the shape properly.
  2. We have defined our function as a class method of UIImage that will return us an image. As we are not contained inside a subclass of UIView, we need to create a graphic context for drawing an image (our canvas), store it in the context variable, and then save it in the stack, because we are going to modify some parameters (like stroke or fill colors).
  3. Next we can add our path to the current context.
  4. Depending on the parameters, we will adjust the stroke color and width, and the fill color. Then it’s time to call CGContextDrawPath. This method is the one that actually performs the drawing of our path. You need to specify one parameter that tells CoreGraphics how should it fill the path (just stroking, filling, stroking and filling…). Previously it was specified with kCGDrawingMode constants, but now starting from Xcode 7 and the new swift, there is a cute enum in its place, so we can use .FillStroke, .Stroke, .Fill, etc.
  5. To extract an image from our canvas, we can call the aptly named method UIGraphicsGetImageFromCurrentImageContext().
  6. Finally, it’s time to restore our context from the stack, end the image context and return our image.

Now we can get a UIImage from any of our paths by simply calling our method with the desired parameters:

There is a basic shapes tutorial playground in my GitHub with all the code involved in this section.

Masking images

One of the coolest uses of UIBezierPaths is masking an image to a custom shape. Many apps use this technique nowadays, specially social networks and Apps with user profiles. This can be done by defining a clipping area or path in our canvas (CGContext). Luckily for us, there is an easy way of doing this directly with our UIBezierPath instance by calling addClip(). This will create a clipping mask in our current context based on the shape defined by the clipping mask. With that in mind, we’ll create a extension for UIImage that will return another image masked to the bounds of our bezier path:

As always, we will start by defining a graphic context (canvas) to paint on. This is because we are not inside the drawRect() method, so we don’t have any context available, and besides, we are generating a new image, so we need a shinny new graphical context (you wouldn’t use the same canvas for two different paintings, now would you?). Then, we just use path.clip() to set the clipping mask and call our super method drawRect() that will draw in the context that we are providing (clipping included). Last, we restore the previous drawing context and return the results.

Given the following original image (taken from Unsplash):

Captura de pantalla 2015-07-26 a las 22.11.03

We can define a circular UIBezierPath to generate an oval image:

Captura de pantalla 2015-07-26 a las 22.10.44

You can download the image masking tutorial playground to see some other examples of custom UIBezierPath applied to images.

Applying UIBezierPaths to UIViews

I think the most powerful use for UIBezierPaths is applying them to UIViews to create custom interfaces, from simple PopUp dialog views to completely customized user interfaces. The possibilities are endless.

Let’s analyze the simple example of building a PopUp dialog. We can do this by means of Layers. In case you don’t know, every UIView is backed by a CALayer, that handles from low-level drawing and graphics stuff to animations, shading, clipping, masking, etc. There are unlimited things you can do with layers, but the one that interests us now is setting a CAShapeLayer with a UIBezierPath. A CAShapeLayer is a special layer that can define the shape of the UIView (actually, the subjacent layer structure of the view). A CAShapeLayer can be fed with a UIBezierPath/CGPath to specify its shape.

The way of doing this is by creating a CAShapeLayer with a given UIBezierPath, and assigning it to the mask property of our UIView’s layer:

The fillRule parameter kCAFillRuleEvenOdd specifies how the layer is filled, using an even-odd rule (more info here). This parameter combined with a solid color (white in this case) tells the shapeLayer to set that zone as opaque. This is just what we want so that when we apply this layer in the mask property of our view, it “cuts” the opaque area.

The popupDialogPath is generated by the method dialogBezierPathWithFrame(…). It receives a frame (supposedly the view’s frame) and the orientation for the dialog arrow (up, down, left…), and builds a UIBezierPath to be applied to the CAShapeLayer:

Points (3) and (4) should be straightforward now, we just create the points for our path, move to the first and add them until closing the path at the end. In (1) and (4) we just make sure that the arrow gets drawn in the right place, given the desired orientation. In order to do this we just perform a simple trick. If the orientation is left or right we just transpose the frame (switching width and height), draw the arrow always in the upper edge, and later apply some rotations at the end to restore it to its right orientation. The rotations must be done also in the case of .Down orientation (in this case, the arrow is drawn at the top and we must rotate all the way down to the bottom). After the rotation, as the path gets rotated from the axis origin (0,0), we need to translate the path back to the beginning of the frame.

The result looks like this:

Captura de pantalla 2015-07-26 a las 22.34.30

 

If you use your imagination, I’m sure you can come with some amazing things by just applying simple bezier paths to your views. You can download the full code in the custom view tutorial playground.

One more thing: IBDesignable custom controls and UIBezierPaths.

I explored on IBDesignable controls on a previous post. Back then I showed how to build a custom control whose changes and status can be seen directly on Interface Builder. As a bonus for this basic tutorial to Bezier paths, we are going to see how we can use UIBezierPaths in the drawRect method of our custom control to draw some interesting things.

Let’s say you want to create a control that shows a circular picture of a user in your social network “How awesome am I?”. This picture will have appended an arc showing the awesomeness level of the user. Something like this (where the blue arc will be larger the more awesome you are ;).

mini1437943790

We will create a custom IBDesignable view called “RoundedImageView” (because I’m not feeling specially creative by now), and we will add some properties to it: the image of the user, the stroke color, stroke width, and a completion level (meaning how awesome you are from 0.0 to 1.0):

You may have noticed that we didn’t inherit from UIImageView, but from UIView. This is because UIImageView’s drawRect will not be called in subclasses, according to Apple:

The UIImageView class is optimized to draw its images to the display. UIImageView will not call drawRect: in a subclass. If your subclass needs custom drawing code, it is recommended you use UIView as the base class.

So we just define it as a UIView’s subclass. We will draw the image ourselves in drawRect(). We also want the “completed” variable to be KVO compliant so our view controller can observe it and change the percentage label accordingly, that’s why we call willChangeValueForKey and didChangeValueForKey.

Now it’s time to define our drawRect method. It will have two main sections. First we’ll draw the user’s image (remember, everything you paint will cover what you’ve previously painted), and then we’ll draw the arc according to the current level of awesomeness indicated by the “completed” variable:

The drawing of the image is pretty straightforward, and it’s the same method we used in the masking images section above. Then if the stroke width is greater that zero, we build a UIBezierPath with an arc, set the parameters and call strokePath.stroke() to draw it.

 

And that’s all for now! You can download the complete project with this custom control in my github repository. (You will need Xcode 7 beta to run the project.)

3 Comments
  • Jusung Kye

    July 28, 2015 at 2:30 am Reply

    Thank you for sharing good tutorial of Bezier Path. :)

  • 1er4y

    February 19, 2016 at 4:35 pm Reply

    Awesome tutorial! Thank you very much and hello from Russia :)

  • David

    May 26, 2017 at 1:03 am Reply

    Thank you for this. It was very helpful.

    A few comments on your shapeImageWithBezierPath(::::) method:

    – the passed-in bezierPath is modified by translating its origin to {0, 0}. This will permanently change the path. To avoid this, we can either perform the transform on a _copy_ of the bezierPath, or the transform should be undone before leaving the function.

    An example of the latter approach:

    let originX = bezierPath.bounds.origin.x
    let originY = bezierPath.bounds.origin.y

    bezierPath.apply(CGAffineTransform(translationX: -originX, y: -originY))

    defer {
    // Undo changes to path position before we leave.
    bezierPath.apply(CGAffineTransform(translationX: originX, y: originY))
    }

    Cheers!

Post a Comment

Before you continue...

Hi there! I created the Digital Tips List, the newsletter for Swift and iOS developers, to share my knowledge with you.


It features exclusive weekly tutorials on Swift and iOS development, Swift code snippets and the Digital Tip of the week.


Besides, If you join the list you'll receive the eBook: "Your First iOS App", that will teach you how to build your first iOS App in less than an hour with no prior Swift or iOS knowledge.

I hate spam, and I promise I'll keep your email address safe.