Swift: Translating and Rotating a CGContext, A Visual Explanation (iOS/Xcode)


The purpose of this post is to demystify as far as possible the process of translating and rotating a CGContext.

Obtaining a reference to a CGContext

Before anything else, it is necessary to obtain a reference to the current context from the drawRect() method of a UIView subclass.
override func drawRect(rect:CGRect)           
{      
   // obtain context     
   let ctx = UIGraphicsGetCurrentContext()
}
The context provides the information on where to draw: "You can think of a graphics context as a drawing destination" (Apple Developer).

Drawing a circle

With the context available to us, let's suppose we draw a circle at the centre of it. The black square represents the context:


In Swift we could add the following code to the drawRect() method in order to draw the circle:
// Decide on radius
let rad = CGRectGetWidth(rect)/3.5
// End angle will be 2*pi for any circle that begins at 0
let endAngle = CGFloat(2*M_PI)
        
// We could use CGContextAddEllipseInRect to draw a circle instead   
CGContextAddArc(ctx, CGRectGetMidX(rect), CGRectGetMidY(rect), rad, 0, endAngle, 1)
// set stroke color
CGContextSetStrokeColorWithColor(ctx,UIColor.whiteColor().CGColor)
// Set line width
CGContextSetLineWidth(ctx, 4.0)

CGContextStrokePath(ctx) 
Note: The code could be simpler, but I want to think here in terms of origins rather than rectangles and so for consistency I've used the CGContextAddArc() method.

Saving the context state

Before any transformation takes place it is important to save the current context state, so that it can be returned to.
CGContextSaveGState(ctx)

Translating the origin of the context

Now the origin of the context can be translated to the origin of the circle like so:



In code this translation is the equivalent to writing
CGContextTranslateCTM(ctx, CGRectGetMidX(rect), CGRectGetMidY(rect))
Note: the circle doesn't move with the context, all that has already been drawn stays where it is.

Rotating the context

Now the context has been translated to a new position it can also be rotated. The rotation happens around the origin of the context.



In Swift the rotation would look like this:
CGContextRotateCTM(ctx, CGFloat(M_PI*45/180))
Again the pre-exisiting drawing does not change.

Drawing on the translated and rotated context

Let's suppose we now draw a line from (0, 0) to (radius, 0) where "radius" is the radius of the circle.

  

The Swift code looks like this:
// create a mutable path
let path = CGPathCreateMutable()
// move to the starting point of the path
CGPathMoveToPoint(path, nil, 0, 0)
// add a line the length of the radius to path
CGPathAddLineToPoint(path, nil, rad, 0)
// add the path to the (translated and rotated) context
CGContextAddPath(ctx, path)
// set line width
CGContextSetLineWidth(ctx, 4)
// set line color
CGContextSetStrokeColorWithColor(ctx,UIColor.whiteColor().CGColor)
// stroke path
CGContextStrokePath(ctx)

Restoring the context

Restoration of the context in code is as simple as the saving of it.
CGContextRestoreGState(ctx)
Once the context is restored back to its original state, this is how things look:


The new line stays exactly where it was drawn and any new drawing uses as its origin the original (0, 0) of the context.

Conclusion

I'm not going to discuss pros and cons of shifting the graphics context around, although Apple does state: "The advantage of modifying the graphics context (as opposed to the path object itself) is that you can easily undo the transformation by saving and restoring the graphics state."

Here I simply wanted to explain what happens and to lay some foundations for a future post. Full code can be found here for you to copy and paste into an initial view controller.





Endorse on Coderwall

Comments

  1. why are u using CGRectGetWidth for radius??

    ReplyDelete
    Replies
    1. it was just a way of ensuring that the circle fits within the rect.

      Delete
  2. Please Can you provide the link of the final project!

    ReplyDelete
  3. You have provided an nice article, Thank you very much for this one. And i hope this will be useful for many people.. and i am waiting for your next post keep on updating these kinds of knowledgeable things...

    Mobile App Development Company in Chennai
    Android app Development Company in Chennai
    ios app development Company in Chennai

    ReplyDelete
  4. Thank you for the clear and detailed explanation! Naturally Swift 4 has done a couple of renames:
    ctx.saveGState()
    ctx.rotate(by: -GraphView.DEG_90)
    ctx.translateBy(x: 0, y: height)
    ctx.restoreGState()

    ReplyDelete
    Replies
    1. Thanks Kevin, I think this post will be worth revisiting at some point. I see many places where I could've been clearer in my explanations, so I'll replace with Swift 4 code then.

      Delete
    2. Question, the call to CGContext.rotate(by:) seems to contradict the doc which says positive values for the angle rotate counter-clockwise?

      Delete

Post a Comment