Exploring SwiftUI: Working with Color Rendering Modes of ImageRenderer
I solely created an app called Meshing to help developers copy mesh gradient code to their SwiftUI project after visualizing it in the app.
One day, I got a message from a designer. They loved the app but did not use Xcode. They just wanted to visualize the gradient and save it as an image. This opened up a whole new audience for me. While the feature set was complete for developers top copy the code, my next focus was on how could I make Meshing work for everyone?
While doing my research, I found out about ImageRenderer. Apple added it to SwiftUI in iOS 16, a class that turns SwiftUI views into images. While this post was initially supposed to be about working with ImageRenderer
, I spiralled into exploring Color Rendering Modes and decided to write about it instead.
Creating Your ImageRenderer
You start by making an ImageRenderer
. You give it a SwiftUI view to work with. It looks like this:
let renderer = ImageRenderer(content: YourSwiftUIView())
Color Rendering Modes
When working with ImageRenderer
, you can choose how you want your colors to be handled. Maybe you can think of it as choosing the color palette for your image. It decides how colors are processed and stored. Apple gives us three options:
- nonLinear
- linear
- extendedLinear
Let's break these down:
1. nonLinear
This is the default mode. It uses the sRGB color space, which is what most screens use.
renderer.colorMode = .nonLinear
Pros:
- It is good for most images you will share or display.
- Colors look natural on most devices.
Cons:
- Cannot safely handle colors outside the 0 to 1 range (produces undefined results).
2. linear
This mode also uses sRGB, but without gamma correction.
renderer.colorMode = .linear
Gamma correction adjusts color brightness to match how our eyes perceive light. It makes screens display colors in a way that looks natural to us. linear
mode skips this adjustment.
Without gamma correction:
- Colors appear as raw values
- Images might look darker than expected
Pros:
- It is great for doing color math or blending.
- What you see is what you get - no hidden adjustments.
Cons:
- Images might look a bit flat or washed out on some screens.
- Like
nonLinear
, cannot safely handle colors outside the 0 to 1 range (produces undefined results).
3. extendedLinear
This is the powerhouse mode. It can handle a wider range of colors.
renderer.colorMode = .extendedLinear
Normally, color values range from 0 (none) to 1 (full intensity). extendedLinear
allows values below 0 or above 1. This captures a wider range of light intensities, like very bright highlights or deep shadows. It is useful for HDR (High Dynamic Range) content or complex lighting scenarios.
For most everyday use, you probably do not need this extended range. But for specialized graphics work or HDR content, it can be invaluable.
Pros:
- It can work with colors outside the normal 0 to 1 range.
- Great for HDR content or when you need extra color precision.
Cons:
- Might not display correctly on all devices.
- Can use more memory.
When to Use Each Mode
- Use
nonLinear
for most everyday images. - Use
linear
if you are doing a lot of color calculations or blending. - Use
extendedLinear
if you are working with HDR content or need to preserve a wide range of color values.
Testing Color Modes
Here is a code to display the same image with different color modes. It is a great way to see which one works best for your needs, by simply replacing SampleGradientView
with your own view:
struct ColorModeComparisonView: View {
@State private var images: [UIImage] = []
var body: some View {
VStack {
ForEach(Array(zip(images, ColorRenderingMode.allCases)), id: \.0) { image, label in
VStack {
Image(uiImage: image)
.resizable()
Text(label.description)
.fontWidth(.expanded)
}
}
}
.task {
generateImages()
}
}
func generateImages() {
images = ColorRenderingMode.allCases.compactMap { mode in
let renderer = ImageRenderer(content: SampleGradientView())
renderer.colorMode = mode
return renderer.uiImage
}
}
}
extension ColorRenderingMode: @retroactive CaseIterable, @retroactive CustomStringConvertible {
public var description: String {
switch self {
case .nonLinear: "Non Linear"
case .linear: "Linear"
case .extendedLinear: "Extended Linear"
@unknown default: ""
}
}
public static var allCases: [ColorRenderingMode] {
[.nonLinear, .linear, .extendedLinear]
}
}
struct SampleGradientView: View {
var body: some View {
LinearGradient(colors: [.purple, .indigo], startPoint: .topLeading, endPoint: .bottomTrailing)
}
}
#Preview("ColorModeComparisonView") {
ColorModeComparisonView()
}
Then I compared it on the iPad:
In my Meshing app, I stuck with nonLinear
which is the default color mode:
func exportGradient() -> UIImage? {
viewModel.showDots = false
let renderer = ImageRenderer(content: GradientPreview(viewModel: viewModel, padding: 0))
return renderer.uiImage
}
I chose nonLinear
because:
- My gradients use standard colors.
- I want the exported images to look good on most devices.
- It is the safest choice for sharing on social media or using in other apps.
But if you are doing something special with colors, do not be afraid to experiment with the other modes!
Wrapping Up
Understanding color modes can help with complex SwiftUI views when working with ImageRenderer
. It is about exporting beautiful pictures and making sure those pictures look great everywhere they are seen.
Have you played around with different color modes in your projects? I would love to hear about your experiences! Message on @rudrankriyam and let's geek out about colors together; maybe I can learn something from you!