WongWray
WongWray

Reputation: 2566

How to specify a minimum UIContentSizeCategory for UIFontMetrics?

I have a method for creating an auto-scaling font based on Dynamic Type that looks like so:

extension UIFont {
    public static func getAutoScalingFont(_ fontName: String, _ textStyle: UIFont.TextStyle) -> UIFont {
        // getFontSize pulls from a map of UIFont.TextStyle and UIFont.Weight to determine the appropriate point size
        let size = getFontSize(forTextStyle: textStyle)
        guard let font = UIFont(name: fontName.rawValue, size: size) else { 
            return UIFont.systemFont(ofSize: size)
        }
        let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
        let traitCollection = UITraitCollection(preferredContentSizeCategory: UIApplication.shared.preferredContentSizeCategory)
        let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle, compatibleWith: traitCollection)
        return fontMetrics.scaledFont(for: font, maximumPointSize: fontDescriptor.pointSize)
    }
}

This seems to work great; when I change the Text Size slider value in the phone Settings, the font scales as necessary.

I'm now trying to add the logic for a minimum UIContentSizeCategory. That is, if the user sets their Text Size value to be less than my specified minimum size category, the font should scale as if they've selected the minimum value.

Here's my attempt:

extension UIFont {
    // This variable represents the minimum size category I want to support; that is, if the user
    // chooses a size category smaller than .large, fonts should be scaled to the .large size
    private static let minimumSupportedContentSize: UIContentSizeCategory = .large

    public static func getAutoScalingFont(_ fontName: String, _ textStyle: UIFont.TextStyle) -> UIFont {
        let size = getFontSize(forTextStyle: textStyle)
        guard let font = UIFont(name: fontName.rawValue, size: size) else { 
            return UIFont.systemFont(ofSize: size)
        }
        // I've extended UIContentSizeCategory to adhere to Comparable so this works fine
        let contentSize = max(UIApplication.shared.preferredContentSizeCategory, minimumSupportedContentSize)
        let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
        let traitCollection = UITraitCollection(preferredContentSizeCategory: contentSize)
        let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle, compatibleWith: traitCollection)
        return fontMetrics.scaledFont(for: font, maximumPointSize: fontDescriptor.pointSize)
    }
}

Via logs I'm able to tell that, as expected, the contentSize I pass into the UITraitCollection initializer is never a value smaller than .large. However, it seems like the value passed to that initializer represents a maximum content size category. That is, if I init the trait collection like so:

let traitCollection = UITraitCollection(preferredContentSizeCategory: .large)

the font will re-scale for all UIContentSizeCategory's smaller than .large but will not re-scale for any categories larger than .large.

Does anyone know how to accomplish setting a minimum UIContentSizeCategory?

Upvotes: 2

Views: 2043

Answers (2)

PGDev
PGDev

Reputation: 24341

Although we have minimumContentSizeCategory and maximumContentSizeCategory supported from iOS 15, we still need the older way in a few scenarios. For example, these 2 properties doesn't work when we need to support dynamic text styles in NSAttributedString.

Here is how I did it the older way,

Use UIApplication.shared.preferredContentSizeCategory to decide which preferredContentSizeCategory to use with UITraitCollection

Example:

func getPreferredFont(textStyle: UIFont.TextStyle, weight: UIFont.Weight? = nil) -> UIFont {
    let preferredContentSizeCategory: UIContentSizeCategory
    switch UIApplication.shared.preferredContentSizeCategory {
    case .extraSmall, .small, .medium:
        preferredContentSizeCategory = .large
    case .accessibilityExtraLarge, .accessibilityExtraExtraLarge, .accessibilityExtraExtraExtraLarge:
        preferredContentSizeCategory =  .accessibilityLarge
    default:
        preferredContentSizeCategory = UIApplication.shared.preferredContentSizeCategory
    }
    let traitCollection = UITraitCollection(preferredContentSizeCategory: preferredContentSizeCategory)
    let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle, compatibleWith: traitCollection)
    let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
    let font: UIFont
    if let weight = weight {
        font = UIFont.systemFont(ofSize: fontDescriptor.pointSize, weight: weight)
    } else {
        font = UIFont.systemFont(ofSize: fontDescriptor.pointSize)
    }
    return fontMetrics.scaledFont(for: font, maximumPointSize: fontDescriptor.pointSize, compatibleWith: traitCollection)
}

Upvotes: 5

atereshkov
atereshkov

Reputation: 4555

Starting from iOS 15 you can set limits on the minimum and maximum sizes of dynamic type:

// UIKit
view.minimumContentSizeCategory = .medium
view.maximumContentSizeCategory = .accessibilityExtraLarge

// SwiftUI
ContentView()
  .dynamicTypeSize(.medium ... .accessibility3) // No smaller than medium, no bigger than accessibility3

When you set one or both of these properties on a view it limits the dynamic type size for that view and any of its subviews.

Since these are properties of UIView they are also available on subclasses like UILabel and UITextView. This gives you fine grained control to limit an individual text element without affecting other text:

label.minimumContentSizeCategory = .large

Here’s a quote from the Apple engineer in the WWDC session:

Please do not use this API to unduly limit text size. These settings serve an extremely important function, and it’s paramount that your app’s functionality is all available, and everything is legible, to people using the highest text size setting.

Upvotes: 1

Related Questions