Swift: Giving context to CGContext (Part I)

First encounters with CGContext can be bewildering. We are presented with a lot of code that looks most unlike Swift and more than likely it's in a drawRect: method of a UIView. So let's consider the entry point to drawing:
override func drawRect(rect: CGRect) {
    if let context = UIGraphicsGetCurrentContext() {
    }
}
To explain, during the lifetime of a drawRect: there is a CGContext that we can gain access to by simply calling UIGraphicsGetCurrentContext() as demonstrated above. Once we have the context, which is the place that we're going to be drawing to, we can draw. And this drawing will appear every time a view of our UIView subclass type is presented on screen.

Demonstrating CGContext

To demonstrate this, the first thing we're going to do is to fill the view and to draw a line around it. But the first thing any writer of Swift code will notice is that placing a dot after the context instance name results in no options popping up whatsoever. So what can we actually do with this context? Well we have to use a range of functions like so:
// drawing code
CGContextAddRect(context, rect)
CGContextSetFillColorWithColor(context, fillColor.CGColor)
CGContextSetStrokeColorWithColor(context, strokeColor.CGColor)
CGContextSetLineWidth(context, width)
CGContextDrawPath(context, CGPathDrawingMode.FillStroke)
And each time we're specifying the context as an argument in addition to other details to get things done. For example, here we want to add a rectangle to the context and not only does it soon become fairly tedious writing long function names and repeating 'context' but if we're commonly creating rectangles with a fill colour and an outline colour then why not simply have an extension that clears away the need for writing all this over and over again and that also makes everything far more Swift-like:
extension CGContextRef {
    func addRect(rect:CGRect, fillColor:UIColor, strokeColor:UIColor, width:CGFloat) {
        CGContextAddRect(self, rect)
        self.fillAndStroke(fillColor, strokeColor: strokeColor, width: width)

    }
    func fillAndStroke(fillColor:UIColor, strokeColor:UIColor, width:CGFloat) {
        CGContextSetFillColorWithColor(self, fillColor.CGColor)
        CGContextSetStrokeColorWithColor(self, strokeColor.CGColor)
        CGContextSetLineWidth(self, width)
        CGContextDrawPath(self, CGPathDrawingMode.FillStroke)
    }

}
So now we can write simply:
context.addRect(CGRect(x: 100, y: 100, width: 200, height: 200), fillColor: UIColor.blueColor(), strokeColor: UIColor.greenColor(), width: 5)
And we can add a filled and stroked rectangle to our view without all the CGContext... functions getting in the way.

The full UIView subclass drawRect: method looking like this:
override func drawRect(rect: CGRect) {
    if let context = UIGraphicsGetCurrentContext() {
        context.addRect(CGRect(x: 100, y: 100, width: 200, height: 200), fillColor: UIColor.blueColor(), strokeColor: UIColor.greenColor(), width: 5)
    }
}
Remembering that the above CGContextRef extensions also need including in our project.

UIBezierPath

A similar functionality already exists via UIBezierPath, which Apple describes as "an Objective-C wrapper for the path-related features in the Core Graphics framework". And you can also create UIBezierPaths within the drawRect: leveraging once again the context but without ever calling UIGraphicsGetCurrentContext().
let bez = UIBezierPath(rect: CGRect(x: 100, y: 100, width: 200, height: 200))
            UIColor.greenColor().setFill()
            UIColor.whiteColor().setStroke()
            bez.lineWidth = 15
            bez.fill()
            bez.stroke()
            
            let bez2 = UIBezierPath(rect: CGRect(x: 200, y: 200, width: 100, height: 100))
            UIColor.greenColor().setFill()
            UIColor.whiteColor().setStroke()
            bez2.lineWidth = 15
            bez2.fill()
            bez2.stroke()
Notice how UIColor provides the setFill and setStroke methods. We don't need any conversion here between UI and CG equivalents. This makes things better but it can feel equally mysterious about how the path is drawn without any acknowledgement of the context and equally no addSubview or addSublayer type methods happening either.

UIBezierPath outside the drawRect

Outside the drawRect of a UIView you can use UIBezierPath in combination with CAShapeLayer, and this is where it feels more natural to me. Although this approach consumes an extra couple of lines of code when placed inside a UIViewController subclass, as here, than it would inside a UIView drawRect: method:
        let bez = UIBezierPath(rect: CGRect(x: 100, y: 100, width: 200, height: 200))
        let shape = CAShapeLayer()
        shape.path = bez.CGPath
        shape.frame = view.frame
        shape.fillColor = UIColor.greenColor().CGColor
        shape.strokeColor = UIColor.blueColor().CGColor
        shape.lineWidth = 15
        view.layer.addSublayer(shape)
And complicates things for simple drawing such as we're doing here.

Conclusion

I am aware that this post is rough around the edges (and in the middle too!) but I wanted to make a start on approaching CGContext and its wrapper, UIBezierPath. It is something that I have plans to return to in the form of bitmap drawing and possibly PDF but we'll see how that goes.

Comments