1

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.

Gergely Kovacs
  • 1,045
  • 2
  • 10
  • 28
  • Unclear what sort of “response” to light / dark mode you are expecting. UIImages do not magically change because the mode changes. You have to pair them with one another. You have no dark mode version of this image to pair it with. – matt Dec 06 '19 at 07:56
  • 1
    I think there is a dark mode version, since Gergely said that the image is correct for the current mode but does not switch automatically. – Frank Rupprecht Dec 06 '19 at 08:08
  • Obviously, I have a light and a dark version in the referenced image asset; otherwise it would be a wee bit problematic to expect any kind of change... – Gergely Kovacs Dec 06 '19 at 08:08
  • “Obviously, I have a light and a dark version in the referenced image asset” Yes, but when you apply `resizableImage` to that, it returns a new different UIImage and obviously that one is not in the asset catalog. That’s all I’m saying. – matt Dec 06 '19 at 19:53
  • Another thought: do you need to set the insets in code or are they always the same? If not, you can try to specify them in the asset catalog (in the Attributes Inspector, at the bottom in the "Slicing" section) and check if the automatic dark mode change would work then. – Frank Rupprecht Dec 07 '19 at 10:50

4 Answers4

2

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

shim
  • 9,289
  • 12
  • 69
  • 108
matt
  • 515,959
  • 87
  • 875
  • 1,141
0

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.

shim
  • 9,289
  • 12
  • 69
  • 108
Frank Rupprecht
  • 9,191
  • 31
  • 56
  • I think it does. When you load the UIImage by name, it's basically a placeholder that resolves to a concrete image depending on the trait environment. However, it seems that `resizableImageWithCapInsets` causes it to resolve to a concrete image (for the current trait environment) and to lose its placeholder status. – Frank Rupprecht Dec 06 '19 at 08:10
  • I will give this a try tonight. Thank you! – Gergely Kovacs Dec 06 '19 at 08:13
  • The gist is not a good model to follow. It tries to reinvent a wheel that doesn't need reinventing. There is a built-in mechanism for this, UIImageAsset. – matt Dec 06 '19 at 21:45
  • Yes, that's what they are using in the gist. – Frank Rupprecht Dec 07 '19 at 10:45
0

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;
    }
}
Gergely Kovacs
  • 1,045
  • 2
  • 10
  • 28
  • No, this is wrong. There is a way to pair any two images, not in the asset catalog, as automatic variants of each other. – matt Dec 06 '19 at 19:55
0

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.

daniel.gindi
  • 3,457
  • 1
  • 30
  • 36