'How to generate a dynamic light/dark mode UIImage from Core Graphics?
iOS 13 introduced UIImage
instances that auto-adopt to the current UIUserInterfaceStyle
(aka light or dark mode). However, there seem to be only methods to construct such images from named or system images (imageNamed:inBundle:withConfiguration:
or systemImageNamed:withConfiguration:
).
Is there a way to dynamically generate a universal light/dark mode UIImage
from Core Graphics (e.g. using two CGImage
s or using UIGraphicsImageRenderer)?
I don't see any API for that but maybe I'm wrong.
Solution 1:[1]
Here's my implementation in Swift 5
extension UIImage {
static func dynamicImage(withLight light: @autoclosure () -> UIImage,
dark: @autoclosure () -> UIImage) -> UIImage {
if #available(iOS 13.0, *) {
let lightTC = UITraitCollection(traitsFrom: [.current, .init(userInterfaceStyle: .light)])
let darkTC = UITraitCollection(traitsFrom: [.current, .init(userInterfaceStyle: .dark)])
var lightImage = UIImage()
var darkImage = UIImage()
lightTC.performAsCurrent {
lightImage = light()
}
darkTC.performAsCurrent {
darkImage = dark()
}
lightImage.imageAsset?.register(darkImage, with: UITraitCollection(userInterfaceStyle: .dark))
return lightImage
}
else {
return light()
}
}
}
This implementation:
- Combines the current traits with the style when evaluating each image (so as to include
displayScale
anduserInterfaceLevel
) - Executes the auto-closures within the correct trait collection (to ensure programmatically generated images are generated correctly)
- But registers the dark image without the current traits, only specifying the dark interface style (so, even if another trait property is modified like
userInterfaceLevel
orhorizontalSizeClass
, usage of the dark image will be unaffected and still used if and only if the interface style is dark)
Example 1
Assume we have two variants already loaded:
let lightImage = ...
let darkImage = ...
let result = UIImage.dynamicImage(withLight: lightImage, dark: darkImage)
Example 2
Assume we want a red image, dynamic for light/dark, simply call:
let result = UIImage.dynamicImage(withLight: UIImage.generate(withColor: UIColor.red),
dark: UIImage.generate(withColor: UIColor.red))
where generate
function is as follows:
extension UIImage {
static func generate(withColor color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(color.cgColor)
context?.fill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image ?? UIImage()
}
}
Solution 2:[2]
You do not create a new UIImageAsset
, instead, you refer one from an existing UIImage
's imageAsset
property, to which you add a dark image variant using UIImageAsset.register(_:with:)
method.
// Prepare a UIImage for light mode.
let lightImage: UIImage!
// Prepare a UIImage for dark mode.
let darkImage: UIImage!
// Register your dark mode image to the light mode image's image asset.
lightImage?.imageAsset?.register(darkImage, with: .init(userInterfaceStyle: .dark))
// Now your light mode image actually becomes a dynamic image. Use it.
someImageView.image = lightImage
someButton.setImage(lightImage, for: .normal)
Or use this UIImage
extension
extension UIImage {
func registerDarkImage(_ image: UIImage) {
if #available(iOS 12.0, *) {
imageAsset?.register(image, with: .init(userInterfaceStyle: .dark))
}
}
}
Solution 3:[3]
Did some research on this some days ago (need this functionality too, but did not implement it so far):
- Create an
UIImageAsset
in code - Register two UIImages using
register(_:with:)
ofUIImageAsset
(supplying userInterfaceStyle .dark / .light) as trait collection parameters https://developer.apple.com/documentation/uikit/uiimageasset/1624974-register
Solution 4:[4]
+ (UIImage*)dynamicImageWithNormalImage:(UIImage*)normalImage darkImage:(UIImage*)darkImage{
if (normalImage == nil || darkImage == nil) {
return normalImage ? : darkImage;
}
if (@available(iOS 13.0, *)) {
UIImageAsset* imageAseset = [[UIImageAsset alloc]init];
// ?? lightImage
UITraitCollection* lightImageTrateCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:
@[[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight],
[UITraitCollection traitCollectionWithDisplayScale:normalImage.scale]]];
[imageAseset registerImage:normalImage withTraitCollection:lightImageTrateCollection];
// ?? darkImage
UITraitCollection* darkImageTrateCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:
@[[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark],
[UITraitCollection traitCollectionWithDisplayScale:darkImage.scale]]];
[imageAseset registerImage:darkImage withTraitCollection:darkImageTrateCollection];
return [imageAseset imageWithTraitCollection:[UITraitCollection currentTraitCollection]];
}
else {
return normalImage;
}
}
maybe, that is what you want.
Solution 5:[5]
Tested on Xcode 13 iOS 14.0 and up
I wanted to avoid using the underlying UIImage imageAsset
property that has been suggested above since the documentation calls out that it can be nil.
I found that by creating the asset manually and registering images against it using the minimum possible trait collections, you can get a dynamic image.
private func createDynamicImage(light: UIImage, dark: UIImage) -> UIImage {
let imageAsset = UIImageAsset()
let lightMode = UITraitCollection(traitsFrom: [.init(userInterfaceStyle: .light)])
imageAsset.register(light, with: lightMode)
let darkMode = UITraitCollection(traitsFrom: [.init(userInterfaceStyle: .dark)])
imageAsset.register(dark, with: darkMode)
return imageAsset.image(with: .current)
}
Below is an illustration using two images, one taken from an asset catalog and the other drawn manually. Both have been set two variants for light and dark mode:
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 | holtmann |
Solution 4 | user7315891 |
Solution 5 |