'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 CGImages 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 and userInterfaceLevel)
  • 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 or horizontalSizeClass, 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()
    }
}

The result: enter image description here

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):

  1. Create an UIImageAsset in code
  2. Register two UIImages using register(_:with:) of UIImageAsset (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