I am migrating some CGContext
code to a UIGraphicsImageRenderer
to render an image.
The new code wraps around the old one like this:
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { imageContext in
let cgContext = imageContext.cgContext
// Here goes the old code
let layer = CGLayer(cgContext, size: size, auxiliaryInfo: nil)!
let layerCGContext = layer.context!
// Do some drawing operations (just an example, much more complex in reality)
cgContext.fill([CGRect(x: 100, y: 100, width: 400, height: 200)])
layerCGContext.fill([CGRect(x: 150, y: 150, width: 200, height: 400)])
// Combine the layer on top of the CGContext layer
cgContext.draw(layer, at: .zero) // <- Code breaks here [1]
}
The above code breaks at [1] if something has been drawn into the layerCGContext
(an empty layer does not trigger the error). If I remove that line the rest of the code works, the image is generated but without the extra layer.
How can I generate multiple layers and recombine them within a UIGraphicsImageRenderer.image{}
closure?
It appears that UIGraphicsImageRenderer
does not support the drawing of CGLayer
into an image context (it crashes). Even if you wrap this in the draw
implementation of a UIView
and then try view.drawHierarchy
, that avoids the crash, but the CGLayer
instances are not rendered from within UIGraphicsImageRenderer
(!).
This seems worthy of a bug report, though that will not help you any time soon. The only workaround that I see is to use the deprecated UIGraphicsBeginImageContext
, UIGraphicsGetImageFromCurrentImageContext
, and UIGraphicsEndImageContext
. The CGLayer
approach works when using that legacy API.
UIGraphicsBeginImageContext(size)
defer { UIGraphicsEndImageContext() }
guard
let cgContext = UIGraphicsGetCurrentContext(),
let layer = CGLayer(cgContext, size: size, auxiliaryInfo: nil),
let layerContext = layer.context
else {
print("unable to get contexts")
return
}
layerContext.setFillColor(UIColor.blue.cgColor)
layerContext.fill([CGRect(x: 150, y: 150, width: 200, height: 400)])
cgContext.draw(layer, at: .zero)
let image = UIGraphicsGetImageFromCurrentImageContext()
The other alternative (which I presume is not easily contemplated in your particular situation) is to eliminate the use of CGLayer
, and just render the graphics directly.
E.g., if you want to render these two rectangles, you can just just fill
on the respective UIBezierPath
instances:
let size = CGSize(width: 600, height: 600)
let image = UIGraphicsImageRenderer(size: size).image { _ in
UIColor.red.setFill()
UIBezierPath(rect: CGRect(x: 100, y: 100, width: 400, height: 200))
.fill()
UIColor.blue.setFill()
UIBezierPath(rect: CGRect(x: 150, y: 150, width: 200, height: 400))
.fill()
}
Or, if you want to use CoreGraphics, you would retrieve the new CGContext
to where you are rendering these rectangles and then call the fill
method:
let image = UIGraphicsImageRenderer(size: size).image { context in
let cgContext = context.cgContext
cgContext.setFillColor(UIColor.red.cgColor)
cgContext.fill([CGRect(x: 100, y: 100, width: 400, height: 200)])
cgContext.setFillColor(UIColor.blue.cgColor)
cgContext.fill([CGRect(x: 150, y: 150, width: 200, height: 400)])
}