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.