Offscreen Rendering

Episode #289 | 12 minutes | published on July 21, 2017 | Uses swift-3.0, Xcode-8.3
Occasionally you will want to do some custom drawing, but not have that presented directly on the screen. Sometimes this is to "bake" the drawing into an image for faster scrolling performance (a single flattened image can be sent to the GPU easily without having to composite a bunch of views together). This is also often used to resize images that are too large for the intended view. This episode covers drawing to a custom context offscreen and obtaining an image out of it.

Links

Creating a Custom Context

Instead of getting the current context, which will be one that is drawn directly to the screen, we can create our own and draw things to that.

We'll need to tell the context what type of bitmap it will be drawing and how big it is.

let scale: CGFloat = 2
let bounds = CGRect(x: 0, y: 0, width: 320, height: 320)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGImageAlphaInfo.premultipliedFirst.rawValue

let context = CGContext(
    data: nil,
    width: Int(bounds.width * scale),
    height: Int(bounds.height * scale),
    bitsPerComponent: 8,
    bytesPerRow: 0,
    space: colorSpace,
    bitmapInfo: bitmapInfo
)!

Preparing the Context for Drawing

We also want to scale our drawing so that we can support the pixel density of the screen we are using. For retina, this means we'll draw things twice as large...

// Apply scale
context.scaleBy(x: scale, y: scale)

You'll notice that when drawing with a custom context, the origin is in the lower-left, unlike UIView which has an origin in the top left. To draw int he same way, we'll have to invert our Y-Axis:

// Invert Y axis
context.translateBy(x: bounds.midX, y: bounds.midY)
context.scaleBy(x: 1, y: -1)
context.translateBy(x: -bounds.midX, y: -bounds.midY)

Drawing

Then we can draw whatever we want:

// Draw square
context.setFillColor(CGColor(colorSpace: colorSpace, components: [0, 0, 1, 1])!)
context.fill(CGRect(x: 20, y: 20, width: 100, height: 100))

Obtaining an Image from our Drawing

let cgImage = context.makeImage()!
UIImage(cgImage: cgImage, scale: scale, orientation: .up) // or NSImage

Alternatively, (on iOS) we can use an image context and get some of the above setup for free:

UIGraphicsBeginImageContextWithOptions(bounds.size, false, scale)
UIColor.blue.setFill()
UIBezierPath(rect: CGRect(x: 20, y: 20, width: 100, height: 100)).fill()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
blog comments powered by Disqus