Content Size Category not being applied
Passing in a prefferedContentSizeCategory into traits doesn't change the image that's outputted.
Is there any additional setup required to get this to work?
Thanks
Hi @zeusmedina! Thanks for the report! We have tests in the code base that exercise this:
https://github.com/pointfreeco/swift-snapshot-testing/blob/4d426f65da1eb4b8f8c7fe2b108ad380ceded10f/Tests/SnapshotTestingTests/SnapshotTestingTests.swift#L378-L397
And a couple example outputs:
- Extra small: https://github.com/pointfreeco/swift-snapshot-testing/blob/4d426f65da1eb4b8f8c7fe2b108ad380ceded10f/Tests/SnapshotTestingTests/Snapshots/SnapshotTestingTests/testTraits.iphone-se-extra-small.png
- Accessibility extra extra extra large: https://github.com/pointfreeco/swift-snapshot-testing/blob/4d426f65da1eb4b8f8c7fe2b108ad380ceded10f/Tests/SnapshotTestingTests/Snapshots/SnapshotTestingTests/testTraits.iphone-se-accessibility-extra-extra-extra-large.png
Can you attach some code that reproduces the issue or submit a PR with a failing test case?
Thank you for the speedy response! Below is the code I'm using. The outputted image shows the labels without the content size category being applied. I'm testing a table view controller, if that changes anything. Maybe I should be testing the cell directly?
assertSnapshot(matching: vc, as: .image(on: iPhoneX, traits: .init(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge)))
Does the view controller handle dynamic content size changes via traitCollectionDidChange?
We currently leverage adjustsFontForContentSizeCategory. Our labels are automatically scaled when larger text is enabled.
A bit more context... I'm testing a table view. My cells have two labels, both with adjustsFontForContentSizeCategory set to true (changing the text size in the emulator works as expected). I also tried overriding traitCollectionDidChange and calling setNeedsLayout and updateConstraintsIfNeeded.
@zeusmedina Hm, I wonder if adjustsFontForContentSizeCategory is just going to be incompatible with trait collection overrides since it's going to depend on an uncontrollable global notification.
Did overriding traitCollectionDidChange work for you? As long as you re-style the fonts inside that method, you should be able to get fonts rendering differently per trait collection.
@zeusmedina I just was able to use adjustsFontForContentSizeCategory and things rendered just fine. Here's a sample test:
func testContentSizeCategory() {
#if os(iOS)
let label = UILabel()
label.adjustsFontForContentSizeCategory = true
label.font = .preferredFont(forTextStyle: .headline)
label.frame.size = .init(width: 320, height: 160)
label.text = "What's the point?"
[
"extra-small": UIContentSizeCategory.extraSmall,
"small": .small,
"medium": .medium,
"large": .large,
"extra-large": .extraLarge,
"extra-extra-large": .extraExtraLarge,
"extra-extra-extra-large": .extraExtraExtraLarge,
"accessibility-medium": .accessibilityMedium,
"accessibility-large": .accessibilityLarge,
"accessibility-extra-large": .accessibilityExtraLarge,
"accessibility-extra-extra-large": .accessibilityExtraExtraLarge,
"accessibility-extra-extra-extra-large": .accessibilityExtraExtraExtraLarge,
].forEach { name, contentSize in
assertSnapshot(
matching: label,
as: .image(traits: .init(preferredContentSizeCategory: contentSize)),
named: "iphone-se-\(name)"
)
}
#endif
}
Can you attach some failing code to reproduce the issue you're having? Sounds like it should be working just fine.
Hi @stephencelis, I'm having a similar problem. Your code works fine since you are setting the frame manually:
label.frame.size = .init(width: 320, height: 160)
but if you let the UILabel decide for itself using label.sizeToFit() it won't work because the images will have the same resolution instead of scaling accordingly
We use Autolayout to size our labels, maybe that's the problem?
I'm not using Auto Layout but just the sizeToFit() func, if this can help
Any news on this @stephencelis?
@giulio92 Sorry, lost track of this! Were you ever able to resolve?
👋 hi @stephencelis. I am hitting the same issue and noticed that the order of operations seem to break snapshots.
In my code I use custom fonts and rely on adjustsFontForContentSizeCategory property to adjust font sizes for Dynamic Type, e.g.
lazy var label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Hello"
// [1] instruct UILabel to adjust font size based on dynamic type
label.adjustsFontForContentSizeCategory = true
// [2] set font
label.font = .preferredFont(forTextStyle: .body)
// or
let customFont = UIFont(name: "MyAwesomeFont-Regular", size: 20)!
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont)
return label
}()
It works perfectly fine in Simulator and on real devices, however with swift-snapshot-testing when testing with custom preferredContentSizeCategory, it only works if I set the font first and then set the adjustsFontForContentSizeCategory to true but not the other way around (label.font = .preferredFont(forTextStyle: .body); label.adjustsFontForContentSizeCategory = true breaks it). And the issue seem to be limited to only cells in UITableView.
Here is how I run the test:
let vc = MyViewControllerWithTableViewInside()
let size = CGSize(width: 480, height: 3000)
let traits: [String: UITraitCollection] = [
"small": .init(preferredContentSizeCategory: .small),
"extraExtraLarge": .init(preferredContentSizeCategory: .extraExtraLarge),
"accessibilityExtraExtraExtraLarge": .init(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge),
]
traits.forEach { (traitName, trait) in
assertSnapshot(matching: vc, as: .image(size: size, traits: trait),
named: "\(traitName).\(Locale.current.languageCode!)")
}
I've added a test case to reproduce this issue in my fork of swift-snapshot-testing: https://github.com/shadone/swift-snapshot-testing/commit/0e354c51440306348977fef95d3099ca37d310b6
I am out of ideas, how would you suggest to investigate this issue further - is it a limitation of UIKit or is it a bug swift-snapshot-testing?
I'm running into this issue as well. My best guess is that adjustsFontForContentSizeCategory relies on the UIContentSizeCategoryDidChangeNotification to work. This notification is only sent when UIApplication.shared.preferredContentSizeCategory is changed but not when a view controller has it's trait collection overridden as snapshot testing does using setOverrideTraitCollection.
I was able to workaround by wrapping my test like:
//Create UITraitCollection you want to use
traits.performAsCurrent {
let view = MyView(model: MyModel())
assertSnapshot(matching: view, as: .image()))
}
In my case the issue is similar, if I calculate size of the label manually, using AttributedString.boundingRect and dynamic font the size is calculated as if the trait collection was default, but then it is rendered with the correct font size. performAsCurrent does not seem to help either.
I imagine that the library could provide a workaround by allowing to inject a closure that would calculate the size of the view instead of accepting already calculated size of the view, and use it to update size before actually making snapshot but after view was placed in the view hierarchy and trait collection was set.
Hi, is there any update on this issue? It would be really great to have this support.
Hi, I was experiencing same issues and after a lot of debugging I made it work!
I solved this issue by firstly creating a profile that doesn’t overrides the UIContentSizeCategory. Keep in mind that all the default ViewImageConfig profiles there are already using UIContentSizeCategoryM and that might conflict with your own request.
// This is existing API
public static func iPhone8(_ orientation: ViewImageConfig.Orientation)
-> UITraitCollection {
let base: [UITraitCollection] = [
// .init(displayGamut: .P3),
// .init(displayScale: 2),
.init(forceTouchCapability: .available),
.init(layoutDirection: .leftToRight),
.init(preferredContentSizeCategory: .medium), // <===== We don’t want this here
.init(userInterfaceIdiom: .phone)
]
switch orientation {
case .landscape:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .compact)
]
)
case .portrait:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .regular)
]
)
}
}
So I created a profile that does not include any traits (because I would use my own array of traits iterating for each content size category and each user interface style), like iPhoneXNeutral
.init(safeArea: safeArea, size: size, traits: .init()) // no traits
After I’ve got my profile created I’ll simply iterate on an array of UITraitCollections composed with my own requirements, in my case I used a combination of all UIContentSizeCategory and UIUserInterfaceStyle (for dark/light modes). And create the snapshots by making on each iteration a Snapshotting with these traits and the neutral profile.
let snapshotting = Snapshotting<UIViewController, UIImage>.image(
on: .iPhoneXNeutral,
traits: traitCollection // make sure this is holding my desired traits (size, light/dark)
)
As last check, you need to make sure that your views and components are changing their sizes accordingly to detect the new trait change by using traitCollectionDidChange… and I used Combine to also refresh upon initialization to the right content size category.
traitCollection.publisher(for: \.preferredContentSizeCategory).sink…
Hope this also help you!
Hi, I've tried every solution offered, but none of them worked without changing the order font > adjustsFontForContentSizeCategory. Is there any update on this issue?
Update: So I guess the order matters :) ( Also if you are using attributedText, it should be set before adjustsFontForContentSizeCategory as well )
