Transforms allow you to draw things rotated, moved, or scaled differently than you specified. You can use this technique to reuse drawing operations where they only differ by some small factor (like drawing lines on a graph), to tilt things like text, or to correct issues where the drawn element is upside down or sideways. In this episode, Sam shares a helpful technique of drawing a grid with a highlighted "origin square" to make it obvious what the transforms are doing.
Links Transforms - Quartz2D Programming Guide CGAffineTransform Class Reference Drawing a Grid to Help Visualize Transformations This utility function is a helpful way of visualizing where your drawing is happening. Not the use of the saveGState and restoreGState to keep this function self contained (in other words, no drawing state will leak out of this method and cause other operations to draw incorrectly). func drawGrid(_ context: CGContext) { context.saveGState() let colorSpace = CGColorSpaceCreateDeviceRGB() let color = CGColor(colorSpace: colorSpace, components: [0, 0, 0, 0.2])! context.setStrokeColor(color) context.setLineWidth(2) // Stroke the border context.stroke(bounds) // Draw a line every 20px let increment: CGFloat = 20 for x in 1..<Int(bounds.height / increment) { // Vertical line context.move(to: CGPoint(x: CGFloat(x) * increment, y: 0)) context.addLine(to: CGPoint(x: CGFloat(x) * increment, y: bounds.height)) for y in 1..<Int(bounds.width / increment) { // Horizontal line context.move(to: CGPoint(x: 0, y: CGFloat(y) * increment)) context.addLine(to: CGPoint(x: bounds.width, y: CGFloat(y) * increment)) } } // Stroke grid context.strokePath() // Draw top left red square context.setFillColor(CGColor(colorSpace: colorSpace, components: [1, 0, 0, 0.5])!) context.fill(CGRect(x: 0, y: 0, width: increment, height: increment)) context.restoreGState() } Zooming Out Zooming out can help you figure out issues where your drawing is happening outside the frame. This is a common situation you might find yourself in when drawing with transforms. For instance if you rotate around the origin 180°, your entire drawing will happen outside the visible rect. When performing transformations like zoom, it is important to remember that it happens at the anchor point, or origin of the context. In order to zoom out from the center, we'll first have to translate so that the anchor point is at the center of the view: context.translateBy(x: bounds.midX, y: bounds.midY) Then we can apply our scale transformation: context.scaleBy(x: 0.5, y: 0.5) And then we just need to jump back to where we were: context.translateBy(x: -bounds.midX, y: -bounds.midY) Flipping Horizontally This is a common thing you might do when showing an image of someone through the camera. Since most people are accustomed to looking at themselves in a mirror, you can simulate this effect by horizontally flipping the image. Flip horizontally context.translateBy(x: bounds.midX, y: bounds.midY) context.scaleBy(x: -1, y: 1) context.translateBy(x: -bounds.midX, y: -bounds.midY) drawGrid(context) Whenever you are dealing with scale transforms, be careful to use 1 (not 0) to represent a no-op, because otherwise that will result in drawing nothing! You might also use this technique to create reflections of objects by drawing them again, scaled -1 in the y dimension in combination with a gradient overlay. Rotating If we want to rotate 45° we first have to convert that to radians. In radians, 2π is equal to the entire circle, so 2π = 360. to convert this to radians: // 45° = 2π * (45/360) // or (simplified)... // 45° = π / 180 It helps to think of thinks in radians, so if 2π is the whole circle, π is half the circle, π/2 is 1/4 circle, and π/4 is 1/8th the circle (or 45°). Again we use the trick to translate to the center so we rotate around the center, then back again after the transformation: // Rotate -45° context.translateBy(x: bounds.midX, y: bounds.midY) context.rotate(by: .pi / -4) context.translateBy(x: -bounds.midX, y: -bounds.midY) drawGrid(context) We can then draw things and they will all be rotated: // Draw blue square context.setFillColor(CGColor(colorSpace: colorSpace, components: [0, 0, 1, 1])!) context.fill(CGRect(x: 60, y: 60, width: 200, height: 200)) This ends up looking like a diamond since we are rotated. It is important to rotate back (or use the graphics state) to go back to how we were: // Rotate 45° (back to regular) context.translateBy(x: bounds.midX, y: bounds.midY) context.rotate(by: .pi / 4) context.translateBy(x: -bounds.midX, y: -bounds.midY) Now we can draw other things that aren’t rotated. // Draw green square context.setFillColor(CGColor(colorSpace: colorSpace, components: [0, 1, 0, 0.5])!) context.fill(CGRect(x: 60, y: 60, width: 200, height: 200)) // Draw yellow square context.setFillColor(CGColor(colorSpace: colorSpace, components: [1, 1, 0, 0.5])!) context.fill(CGRect(x: 300, y: 300, width: 20, height: 20))