'UIImage with resizableImageWithCapInsets Does Not Respond in Dark Mode

Does anyone know of a way to make a UIImage that has been stretched with resizableImageWithCapInsets respond to changes in light/dark mode? My current implementation only takes into consideration dark/light mode when it is being drawn the first time.

[thumbnailContainer addSubview:[self addTileBackgroundOfSize:thumbnailContainer.bounds]];

- (UIImageView *) addTileBackgroundOfSize:(CGRect)bounds {
    UIImageView *backgroundView = [[UIImageView alloc] initWithFrame:bounds];
    UIEdgeInsets insets         = UIEdgeInsetsMake(10.0f, 49.0f, 49.0f, 10.0f);
    UIImage *backgroundImage    = [[UIImage imageNamed:@"UnivGalleryTile"] resizableImageWithCapInsets:insets];
    backgroundView.image        = backgroundImage;

    return backgroundView;
}

I guess I could redraw them in a traitCollection delegate method but I was hoping there is a better way to make them respond.



Solution 1:[1]

First of all, there is no surprise here. When you say resizableImage, you make a new image. It is no longer the image you got from the asset catalog, so it has lost the automatic linkage / dynamism that makes an image change automatically to another image when the trait collection changes.

Second, that doesn't matter, because you can create that linkage with any two images (that are not in the asset catalog). You do that by way of the UIImageAsset class.

So here's a working example. Imagine that Faces is the name of a pair in the asset catalog, one for Any, one for Dark. I'll extract each member of the pair, apply resizable to each one, and then join the new pair together as variants of one another:

let tclight = UITraitCollection(userInterfaceStyle: .light)
let tcdark = UITraitCollection(userInterfaceStyle: .dark)
var smiley = UIImage(named: "Faces", in: nil, compatibleWith: tclight)!
var frowney = UIImage(named: "Faces", in: nil, compatibleWith: tcdark)!
let link = UIImageAsset()
let insets = UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 30)
smiley = smiley.resizableImage(withCapInsets: insets)
frowney = frowney.resizableImage(withCapInsets: insets)
link.register(smiley, with: tclight)
link.register(frowney, with: tcdark)

Or in Objective-C:

UITraitCollection* tclight = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight];
UITraitCollection* tcdark = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
UIImage* smiley = [UIImage imageNamed:@"Faces" inBundle:nil compatibleWithTraitCollection:tclight];
UIImage* frowney = [UIImage imageNamed:@"Faces" inBundle:nil compatibleWithTraitCollection:tcdark];
UIImageAsset* link = [UIImageAsset new];
UIEdgeInsets insets = UIEdgeInsetsMake(30, 30, 30, 30);
smiley = [smiley resizableImageWithCapInsets:insets];
frowney = [frowney resizableImageWithCapInsets:insets];
[link registerImage:smiley withTraitCollection:tclight];
[link registerImage:frowney withTraitCollection:tcdark];

All done. Notice that in the code there is no need to retain any of the objects (link, smiley, frowney). Now if you insert one member of the pair into, say, an image view, it will change to the other automatically when the user light/dark mode changes:

let tc = self.traitCollection
let im = link.image(with: tc)
self.imageView.image = im

I'll switch back and forth between light and dark mode to prove that this is working:

enter image description here

Solution 2:[2]

I have solved it, but boy is this ugly. So, if anyone has a nicer solution I am open to it:

I first store the image view in an NSMutableArray:

- (UIImageView *) addTileBackgroundOfSize:(CGRect)bounds {
    UIImageView *backgroundView     = [[UIImageView alloc] initWithFrame:bounds];
        UIEdgeInsets insets         = UIEdgeInsetsMake(10.0f, 49.0f, 49.0f, 10.0f);
        UIImage *backgroundImage    = [[UIImage imageNamed:@"UnivGalleryTile"] resizableImageWithCapInsets:insets];
    backgroundView.image            = backgroundImage;

    // Store image for re-drawing upon dark/light mode change
    [thumbnailArray addObject:backgroundView];

    return backgroundView;
}

And then I reset the background image manually when the user changes the screen mode:

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
    for (int i = 0; thumbnailArray.count > i; i++) {
        UIEdgeInsets insets         = UIEdgeInsetsMake(10.0f, 49.0f, 49.0f, 10.0f);
        UIImage *backgroundImage    = [[UIImage imageNamed:@"UnivGalleryTile"] resizableImageWithCapInsets:insets];

        ((UIImageView *)[thumbnailArray objectAtIndex:i]).image = backgroundImage;
    }
}

Solution 3:[3]

It seems resizableImageWithCapsInsets causes the image to lose its dynamic, auto-adapting properties. You could maybe try to create images for both appearances and put them together again into a dynamic image. Check out this gist on how this could be done.

Solution 4:[4]

In case of a .tiled image with .zero insets - there's a UIKit bug that removed the configuration, as it only checks for non-zero insets, and does not take into account a case of zero insets with tiled configuration.

A workaround is to do:

let responsiveZeroEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0000001)
let darkImage = yourDarkImage.resizableImage(withCapInsets: responsiveZeroEdgeInsets, resizingMode: .tile)
let lightImage = yourLightImage.resizableImage(withCapInsets: responsiveZeroEdgeInsets, resizingMode: .tile)

And then put them into the asset.
The trick is to use 0.0000001 insets.

I've opened a bug report with Apple: #9997202.

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 shim
Solution 2 Gergely Kovacs
Solution 3 shim
Solution 4 daniel.gindi