Skip to content

Instantly share code, notes, and snippets.

@timonus
Last active January 1, 2024 12:08
Show Gist options
  • Star 54 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save timonus/8b4feb47eccb6dde47ca6320d8fc6b11 to your computer and use it in GitHub Desktop.
Save timonus/8b4feb47eccb6dde47ca6320d8fc6b11 to your computer and use it in GitHub Desktop.
Programmatically create iOS 13 dynamic images
- (UIImage *)dynamicImage
{
UITraitCollection *const baseTraitCollection = /* an existing trait collection */;
UITraitCollection *const lightTraitCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:@[baseTraitCollection, [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]];
UITraitCollection *const purelyDarkTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
UITraitCollection *const darkTraitCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:@[baseTraitCollection, purelyDarkTraitCollection]];
__block UIImage *lightImage;
[lightTraitCollection performAsCurrentTraitCollection:^{
lightImage = /* draw image */;
}];
__block UIImage *darkImage;
[darkTraitCollection performAsCurrentTraitCollection:^{
darkImage = /* draw image */;
}];
[lightImage.imageAsset registerImage:darkImage withTraitCollection:purelyDarkTraitCollection];
return lightImage;
}
@JunyiXie
Copy link

App background switch, invalid

@JunyiXie
Copy link

JunyiXie commented Nov 20, 2019

fix image configuration

+ (instancetype)imageWithLightImageBlock:(UIImage *(^)(void))lightImageBlock
                          darkImageBlock:(UIImage *(^)(void))darkImageBlock
{
    
    __block UIImage *image = nil;
    if (@available(iOS 13.0, *)) {
        UITraitCollection *const scaleTraitCollection = [UITraitCollection currentTraitCollection];
        
        UITraitCollection *const lightUnscaledTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight];
        UITraitCollection *const darkUnscaledTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
        
        
        UITraitCollection *const lightScaledTraitCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:@[scaleTraitCollection, lightUnscaledTraitCollection]];
        UITraitCollection *const darkScaledTraitCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:@[scaleTraitCollection, darkUnscaledTraitCollection]];
        
        [lightScaledTraitCollection performAsCurrentTraitCollection:^{
            image = lightImageBlock();
            image = [image imageWithConfiguration:[image.configuration configurationWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]];
        }];
        __block UIImage *darkImage;
        [darkScaledTraitCollection performAsCurrentTraitCollection:^{
            darkImage = darkImageBlock();
            darkImage = [darkImage imageWithConfiguration:[darkImage.configuration configurationWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]]];
        }];
        
        [image.imageAsset registerImage:darkImage withTraitCollection:darkScaledTraitCollection];
    } else {
        image = lightImageBlock();
    }
    return image;
}

@1marcosgn
Copy link

This implementation worked for me 🚀 (Tested with background switch from dark -> light and the other way around)

+ (instancetype _Nonnull)imageWithLightImageBlock:(UIImage *_Nonnull(^_Nonnull)(void))lightImageBlock darkImageBlock:(UIImage *_Nonnull(^_Nonnull)(void))darkImageBlock;
{
    __block UIImage *image = nil;
    
    if (@available(iOS 13.0, *)) {
        
        UITraitCollection *const scaleTraitCollection = [UITraitCollection currentTraitCollection];
        
        UITraitCollection *const lightUnscaledTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight];
        UITraitCollection *const darkUnscaledTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
        
        UITraitCollection *const lightScaledTraitCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:@[scaleTraitCollection, lightUnscaledTraitCollection]];
        UITraitCollection *const darkScaledTraitCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:@[scaleTraitCollection, darkUnscaledTraitCollection]];
        
        [darkScaledTraitCollection performAsCurrentTraitCollection:^{
            image = lightImageBlock();
            image = [image imageWithConfiguration:[image.configuration configurationWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]];
        }];
        
        __block UIImage *darkImage;
        
        [lightScaledTraitCollection performAsCurrentTraitCollection:^{
            darkImage = darkImageBlock();
            darkImage = [darkImage imageWithConfiguration:[darkImage.configuration configurationWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]]];
        }];
        
        [image.imageAsset registerImage:darkImage withTraitCollection:darkScaledTraitCollection];
        
    } else {
        image = lightImageBlock();
    }
    return image;
}

ezgif com-video-to-gif

@SuperWomble
Copy link

Thanks to all contributors for their help.

I discovered that the traits must include a valid displayScale. Otherwise, bad things happen. (Most notably: UIButtons with a dynamic UIImage background will have an incorrect, doubled intrinsicContentSize applied.)

The following adaption of n8chur's code works for me:

static func dynamicImageWith(
      light makeLight: @autoclosure () -> UIImage,
      dark makeDark: @autoclosure () -> UIImage)
      -> UIImage
  {
      let image = UITraitCollection(userInterfaceStyle: .light).makeImage(makeLight())
      let scaleTrait = UITraitCollection(displayScale: UIScreen.main.scale)
      let styleTrait = UITraitCollection(userInterfaceStyle: .dark)
      let traits = UITraitCollection(traitsFrom: [scaleTrait, styleTrait])
      image.imageAsset?.register(makeDark(), with: traits)
      return image
  }

@dangalasko
Copy link

Perhaps this is controversial but I found (on Xcode 13, iOS 14 and up) that if you start with an image asset and register the variants manually this works too:

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

Here is what I see on an iOS 14 device using two distinct images. One from the Asset Catalog and the other is manually drawn.

@ULazdins
Copy link

@dangalasko , just tried and it doesn't work for me on Xcode 13.1 and iOS 15 device

The code returns dark image for both light and dark modes

Cursor_and_AppDelegate_swift

@dotswift
Copy link

dotswift commented Feb 9, 2022

This is what worked for me on XCode 13.2 / iOS 15 📦

    static func dynamicImage(light: @autoclosure () -> UIImage, dark: @autoclosure () -> UIImage) -> UIImage {

        let imageAsset = UIImageAsset()
        let lightTraitCollection = UITraitCollection(traitsFrom: [.init(userInterfaceStyle: .light)])
        let darkTraitCollection = UITraitCollection(traitsFrom: [.init(userInterfaceStyle: .dark)])
        imageAsset.register(dark(), with: darkTraitCollection)
        imageAsset.register(light(), with: lightTraitCollection)
        return imageAsset.image(with: .current)
    }

@cragod
Copy link

cragod commented Jul 21, 2022

After creating an imageAsset and registering the image with traitCollection more than 65535 times, the image taken by imageAsset.image(with:) will always be wrong.
P.S. Tried all of the above solutions.

@lafezhang
Copy link

After creating an imageAsset and registering the image with traitCollection more than 65535 times, the image taken by imageAsset.image(with:) will always be wrong. P.S. Tried all of the above solutions.

@cragod did you find any solution?

@cragod
Copy link

cragod commented Dec 28, 2023

After creating an imageAsset and registering the image with traitCollection more than 65535 times, the image taken by imageAsset.image(with:) will always be wrong. P.S. Tried all of the above solutions.

@cragod did you find any solution?

not yet, just use the cache to delay its occurrence.

@lafezhang
Copy link

After creating an imageAsset and registering the image with traitCollection more than 65535 times, the image taken by imageAsset.image(with:) will always be wrong. P.S. Tried all of the above solutions.

@cragod did you find any solution?

not yet, just use the cache to delay its occurrence.

The reason has been identified:

  1. When creating UIImageAsset and registering images, Apple will use an auto-increment as the identifier for the image asset and cache it in a global container. And it will also bind the identifier to the imageAsset object. You can inspect the auto-increment identifier using po [[UIImage new] valueForKeyPath:@"imageAsset._unsafe_mutableCatalog._themeStore._maxNameIdentifier"] in the debugger.
  2. However, when retrieving from the cache, Apple will the identifier = {original identifier} & 0xffff. So, the identifier 65536 becomes 1, which causes the unexpected result.
    image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment