Paulo Andrade

Keeper of Secrets.

twitter github stackoverflow linkedin email
@DynamicFont
Sep 18, 2019
3 minutes read

Undoubtedly one of the biggest features developers need to tackle for iOS 13 is Dark Mode. Needless to say I’ve been hard at work to bring that to Secrets. Not only that but, since I’m reviewing all app screens I’m also adding support for Dynamic Type. To that effect I’ve been working on a new UI component written in Swift.

This component (technically a framework) should contain all presentation related code. For example, for colours I’m using an asset catalog to compile a list semantic colors I use throughout Secrets. I then use SwiftGen to generate constants for those colors.

But that’s not what this post is about, I want to talk about how Swift has allowed me to make my code just a bit more cleaner.

Dynamic Type

This new UI component also needs to list the fonts I’m using. There’s no asset catalog for fonts (yet), so this started out just being a plain struct I maintain by hand.

public struct Fonts {
    public var cardTitle = UIFont.systemFont(ofSize: 16)
    public var cardSubtitle = UIFont.systemFont(ofSize: 14, weight: .light)
}

But to support Dynamic Type I also need to specify how the font scales when the content size changes. This is done using the scaledFontFor: method.

public var cardTitle = UIFontMetrics(forTextStyle: .body)
			.scaledFont(for: UIFont.systemFont(ofSize: 16))

Easy right? Well no. Turns out this actually doesn’t work on iOS 11. Because the result of that expression is not a dynamic font but the correct font for the current content size. So the expression needs to be evaluated at the point of use not when the struct is first instantiated. I could change that to be a derived property:

public var cardTitle: UIFont {
	return UIFontMetrics(forTextStyle: .body)
		.scaledFont(for: UIFont.systemFont(ofSize: 16))
} 

But I’d loose the ability to change fonts at runtime. I could make it a closure:

public var cardTitle: () -> UIFont = {	
	return UIFontMetrics(forTextStyle: .body)
		.scaledFont(for: UIFont.systemFont(ofSize: 16))
}

That would work… but definitely smells like 💩.

Property Wrappers

With all the fussing I thought this seemed like a good opportunity to try out property wrappers.

The annotation would simply make the font be scaled and optionally be parameterised with a text style and/or a maximum point size.

@DynamicFont
public var cardTitle = UIFont.systemFont(ofSize: 16)
@DynamicFont(withMetricsForTextStyle: .body, maximumPointSize: 26)
public var cardInput = UIFont.systemFont(ofSize: 16, weight: .light)

This makes sure the scaling is done at the point of use and looks so much cleaner.

And all the code to do this is just about 30 lines1:

@propertyWrapper
public struct DynamicFont {
    public var textStyle: UIFont.TextStyle
    public var maximumPointSize: CGFloat?
    
    public var baseFont: UIFont
    public var wrappedValue: UIFont {
        get {
            let metrics = UIFontMetrics(forTextStyle: textStyle)
            if let mps = maximumPointSize {
                return metrics.scaledFont(for: baseFont, maximumPointSize: mps)
            } else {
                return metrics.scaledFont(for: baseFont)
            }
        }
        set { baseFont = newValue }
    }
    
    public init(wrappedValue font: UIFont, withMetricsForTextStyle textStyle: UIFont.TextStyle, maximumPointSize: CGFloat?) {
        self.textStyle = textStyle
        self.baseFont = font
        self.maximumPointSize = maximumPointSize
    }
    
    public init(wrappedValue font: UIFont, withMetricsForTextStyle textStyle: UIFont.TextStyle) {
        self.init(wrappedValue: font, withMetricsForTextStyle: textStyle, maximumPointSize: nil)
    }
    
    public init(wrappedValue font: UIFont) {
        self.init(wrappedValue: font, withMetricsForTextStyle: .body, maximumPointSize: nil)
    }
}

Sounds like a good deal to me.


  1. It could even be less lines if I could collapse all 3 initialisers into a single initialiser with default parameters but alas… the Swift compiler currently segfaults if I do that. [return]


Back to posts