Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
Update 7/02/18 (The Quartz API has undergone some radical changes over the years. We’re updating our popular Core Graphics series to work well with the current version of Swift, so here is an update to the second installment.)
In medias res? Check out part one of our posts on Core Graphics.
The context lies at the heart of Quartz: you need to interact with the current Core Graphics context in some manner to actually draw stuff, so it’s good to get comfortable with it, what it does, and why it’s there.
One fundamental operation in Core Graphics is creating a path. A path is a mathematical description of a shape. A path can be a rectangle, or a circle, a cowboy hat, or even the Taj Mahal. This path can be filled with a color—that is, all points within the path are set to a particular color. The path can also be outlined, a.k.a. stroked. This is like taking a calligraphy pen and drawing around the path leaving an outline. Here’s a hat that’s been stroked, filled, and then both filled with yellow and stroked in blue:
As you can see, the actual outline can get pretty complex. It can be drawn in a particular color. The line can have a dash pattern. It could be stroked with a wide line or a narrow line. The ends of lines can have square or round ends, and on and on and on. That’s a lot of attributes.
If you peruse the Core Graphics API, you won’t see a call that takes all the settings:
CoreGraphics.stroke(path: path, color: green, lineWidth: 2.0,
dashPattern: dots, bloodType: .oPositive, endCap: .miter)
Instead, you have this call:
context.strokePath()
Where do all those extra values come from, then? They come from the context.
The context holds a pile of global state about drawing, which are a bunch of independent values:
That’s a lot of state. The entire set of state that Core Graphics maintains is undocumented, so there may be even more settings lurking under the hood. Different kinds of contexts (an image vs. a PDF, for example) may contain additional settings.
Whenever Core Graphics is told to draw something, such as “fill this rectangle,” it looks to the current context for the necessary bits of drawing info. The same sequence of code can have different results depending on what’s in the context. On one hand, this is very powerful: a generic bit of drawing code can be manipulated via the context into dramatically different results. On the other hand, the context is a big pile of global state, and global state is easy mess up unintentionally.
Say you have code like this:
draw orange square:
set color to orange in the current context
fill a rectangle
You’ll end up with an orange square. Now assume you’re drawing a valentine too:
draw red valentine:
set color to red in the current context
fill a valentine
Yay! A red heart. Now say you add the valentine drawing code in your first function:
draw orange square:
set color to orange in the current context
draw red valentine
fill a rectangle
Your rectangle will come out red instead of orange. Why? The valentine drawing code has clobbered the current drawing color. The color used to be orange by the time you filled the rectangle, but now it’s red. How can you avoid bugs like this?
There are two approaches. One way is to save off state before you change it—if you’re changing the global color, save off the current color, change it, do your drawing, and then restore it. That’s ok with one or two parameters, but doesn’t scale if you’re changing a dozen of them. There are also some context values that can get changed as side effects, so you’d have to account for those. Oh, and it’s actually impossible to do in Core Graphics because there are no getters for the current context. Sorry about that.
The other approach is to save the entire context before you change anything. Save the context, make your adjustments to the color or line width, do your drawing, and then restore the entire context. The Core Graphics API provides calls to save and restore the settings of the current context. These settings are known as the graphics state, or GState. A Core Graphics context keeps a stack of GStates behind the scenes.
Saving a context’s settings means you are pushing a copy of the settings on to the context’s stack. When you restore the graphics state, the previously saved GState gets popped off the stack and becomes the context’s current set of values, undoing any changes you may have made.
Changing the valentine drawing code like this fixes the “orange rectangle is red” bug:
draw red valentine:
save graphics state
set color to red in the current context
fill a valentine
restore graphics state
Then, the entire sequence of drawing calls will look like this:
set color to orange in the current context
save graphics state
set color to red in the current context
fill a valentine
restore graphics state
fill a rectangle
Here are the GState manipulations for this sequence of drawing calls. Time moves from left to right:
There are a couple of flavors of the CG API. One is the Core Foundation / C-based version used in C, C++, and Objective-C. It uses pseudo-object opaque types such as CGContextRef
or CGColorRef
. It’s pretty old-school with a lot of C functions that take their “objects” as the first parameter, and then a pile of arguments afterwards. Swift has overlays that provide a Swifty API on top of the C API. The Swift API is what I’ll be talking about here and in future postings.
CGContext
is the Swift type for a core graphics context. Usually you’ll get this context from your UI toolkit. In desktop Cocoa you ask NSGraphicsContext
:
let context = NSGraphicsContext.current()?.cgContext // type is CGContext?
and UIKit:
let context = UIGraphicsGetCurrentContext() // type is CGContext?
You can also get contexts that render into a bitmap image (check out UIGraphicsBeginImageContext
and friends).
Once you have a context, you can do things like change the color color, change the line width, or tell it to stroke/fill specific shapes.
For example, this outlines a rectangle:
let context = ... // CGContext
let bounds = someThing.bounds // CGRect
context.stroke(bounds)
We’ve got more information about rectangles (part 1, part 2) and paths for the more curious.
CGContext.stroke(_:)
outlines a given rectangle. If the context is an image context, or the context used to render graphics on the screen, a rectangle’s-border worth of pixels will be laid down using the context’s current settings. If you’re drawing into a PDF context, then a couple of bytes of instructions are recorded to ultimately outline a rectangle when the PDF is rendered at some future time.
GrafDemo is a Cocoa desktop app that demonstrates various parts of Core Graphics for this series of postings. You can poke around that for examples of CG code.
GrafDemo’s Simple demo contains an NSView
that draws a green circle, surrounded by a thick blue line, on a white background, with a thin black border around the entire view.
There are two versions of the code: one that has good GState hygiene and one that doesn’t. Notice that in the sloppy version, the thick blue line leaks out and is contaminating the border. (When you run the program you’ll actually see two views side-by-side when you run the program. One is implemented in Objective-C and the other in Swift.)
There’s a convenience property for getting the current context from inside of an
NSView’s draw(_:)
method:
extension NSView {
var currentContext : CGContext {
let context = NSGraphicsContext.current()
return context!.cgContext
}
}
You can make a similar extension on UIView
to unify accessing the current context.
Here’s the sloppy drawing method:
func drawSloppily () {
let context = currentContext
context.setStrokeColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) // Black
context.setFillColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) // White
context.setLineWidth(3.0)
drawSloppyBackground()
drawSloppyContents()
drawSloppyBorder()
}
The background and border methods are pretty straightforward:
func drawSloppyBackground() {
currentContext.fill(bounds)
}
func drawSloppyBorder() {
currentContext.stroke(bounds)
}
They both assume the context is configured the same way that draw(_:)
set it up. But! There is a problem:
func drawSloppyContents() {
let innerRect = bounds.insetBy(dx: 20.0, dy: 20.0)
let context = currentContext
context.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // Green
context.fillEllipse(in: innerRect)
context.setStrokeColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0) // Blue
context.setLineWidth(6.0)
context.strokeEllipse(in: innerRect)
}
Notice the changes to the color and line width. The current context holds a pile of global state, so the existing fill and stroke color, and the existing line width, totally get clobbered.
The way to fix this problem is to copy the graphics context before drawing the contents. CGContext.saveGState()
pushes a copy of the existing graphics context/graphics state onto a stack. CGContext.restoreGState()
pops off the top of the stack and replaces the current context.
Here’s a nicer version of the content drawing that saves the graphics state:
func drawNiceContents() {
let innerRect = bounds.insetBy(dx: 20.0, dy: 20.0)
let context = currentContext
context.saveGState() // Push the current context settings
context.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // Green
context.fillEllipse(in: innerRect)
context.setStrokeColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0) // Blue
context.setLineWidth(6.0)
context.strokeEllipse(in: innerRect)
context.restoreGState() // Pop them off and undo
}
Wrapping a save/restoreGState around the drawing prevents this method from polluting other methods.
Because this drawing happens inside of a “scope” defined by GState saving and restoring,
I like to make that scope explicit in my code – this code is unambigiously protected
by saving the GState, without having to scan for save/restoreGState calls. You can even see
me making a scope via indentation in the orange-rectangle / red-heart example earlier.
I have another extension that wraps a GState push/pop in a closure:
import CoreGraphics
extension CGContext{
func protectGState(_ drawStuff: () -> Void) {
saveGState()
drawStuff()
restoreGState()
}
}
Which makes the more hygenic drawing look like this:
func drawNiceContents() {
let innerRect = bounds.insetBy(dx: 20.0, dy: 20.0)
let context = currentContext
context.protectGState {
context.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // Green
context.fillEllipse(in: innerRect)
context.setStrokeColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0) // Blue
context.setLineWidth(6.0)
context.strokeEllipse(in: innerRect)
}
}
OK, so what about Objective-C? GrafDemo has parallel Swift and Objective-C implementations, so feel free to peruse the sample code of your choice.
Back in the old days, Objective-C and Swift use of Core Graphics was nearly identical.
That made sharing source code easy (copy and paste, search and replace semicolons, tweak
your variable declaratons.) Swift 3 converted the old C-based API into
a nice object-oriented API.
The actual operations are identical – save a gstate, set a color, make a path, stroke or
fill a color, restore a gstate. They’re just spelled differently.
OK, so what about other Apple platforms? Core Graphics is a lower-level framework that lives below AppKit, UIKit (iOS and TVos), and WatchKit. This means that your Core Graphics code can be pretty much identical on macOS and iOS. The main differences are how you initially get a context to draw in to (which is easily hidden behind extensions) and some of the more esoteric functions are only avaialble on macOS. You also don’t have easy cross-platform access to the higher-level abstractions (e.g. UIBezierPath / NSBezierPath and UIImage / NSImage).
The higher-level APIs do mix and match well with the lower-level ones. For example, you can push a GState, then use UIColor.purple.set() to change the drawing color, and then fill/stroke a path.
This time, you met Core Graphics contexts, which are buckets of various drawing attributes. A context is an opaque structure, so you have no real idea what is really lurking inside. Because of this opaque state, and given the fact that some Core Graphics calls come with side effects, it’s impossible to save drawing attributes before changing them.
Core Graphics has the concept of a graphics state stack, which comes from Postscript. You can push a copy of the current graphics state onto a stack with CGContext.saveGState()
and can undo any changes made to the context by popping the saved state with CGContext.restoreGState()
. Got code that’s polluting the context so that subsequent drawing is wrong? Wrap it in a Save/Restore.
Core Graphics code is nearly identical across Apple’s platforms, so Core Graphics code can be pretty portable amongst the different parts of the Apple ecosystem.
Coming up next time: Lines! (as in, Lines-excitement-yay-happy-fun-times, not Lines-implicity-unwrapped-optionals.)
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...