Search code examples
iosuiimageuikitios-darkmodeuiuserinterfacestyle

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

  • 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