iosswiftuifontuitraitcollectionuifontdescriptor

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?


Solution

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