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:


    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