17

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.

Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213

5 Answers5

11

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

Rui L
  • 251
  • 2
  • 7
  • 4
    This `.init(userInterfaceStyle: .dark)` gave me size problems, I think it's better to init the trait collection like this `UITraitCollection(traitsFrom: [.current, .init(userInterfaceStyle: .dark)])` if you are on iOS13+ – lawicko Aug 30 '21 at 15:43
8

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

beebcon
  • 6,893
  • 5
  • 25
  • 27
  • After rotation incorrect image is shown. @user7315891 has proper solution, without using current trait collection – Igor Palaguta Apr 14 '22 at 07:32
  • Thanks @IgorPalaguta! I've updated my answer, you were right. While the current traits are still used when evaluating the image auto-closures, the dark image is now only registered with the dark trait collection. I've updated the bulleted points as well to properly explain. – beebcon Apr 15 '22 at 17:27
4

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
holtmann
  • 6,043
  • 32
  • 44
  • 5
    Once you've got a `UIImageAsset` with two `UIImage` objects registered, what can you do with it? I can't see any way of displaying it in a `UIImageView`. – Adam Swinden Jan 23 '20 at 12:43
3
+ (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.

2

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:

Daniel Galasko
  • 23,617
  • 8
  • 77
  • 97
  • 1
    This will return a 1 time resolved image from the current trait applied to the asset catalogue. The UIImage instance returned will not be backed by a proper ImageAsset unless it's loaded from an xcasset folder unfortunately. What this means is that the image returned from above will not automatically resolve a trait collection after being assigned to a UIImageView and must be re-resolved each time a trait collection changes. – TheCodingArt Nov 10 '21 at 15:50
  • @TheCodingArt I updated to explain my setup. Not sure if you have tested things but this works fine for me and the GIF illustrates as such... – Daniel Galasko Nov 11 '21 at 07:01
  • I was actually able to run my old code from a year ago or so and validated that this issue seems to have been corrected with iOS updates (which is frankly wonderful). This seems to work as the interface implies it should using Xcode 13 on multiple trait environments and orientations! – TheCodingArt Nov 11 '21 at 14:14