How to use Dynamic Type with a custom font
How to use Dynamic Type with a custom font êŽë š
Updated for Xcode 15
If youâre using iOS 14 or later youâll find your custom fonts scale automatically with no further work from you. However, if you want your fonts to scale relative to a specific Dynamic Type font, you should use the relativeTo
parameter like this:
Text("Scaling")
.font(.custom("Georgia", size: 24, relativeTo: .headline))
That will start the font at 24pt, but it will scale up and down relative to the Headline Dynamic Type font.
If you want to disable Dynamic Type for your font â if you want to fix the font size so it never changes regardless of the Dynamic Type setting â then you should replace size
with fixedSize
when creating your custom font, like this:
VStack {
Text("This Scales")
.font(.custom("Georgia", size: 24))
Text("This is Fixed")
.font(.custom("Georgia", fixedSize: 24))
}
If you need to target iOS 13 then continue reading belowâŠ
SwiftUI comes with support for all of Dynamic Typeâs font sizes, all set using the .font()
modifier. However, if you ask for a specific font and size, youâll find your text no longer scales up or down automatically according to the userâs Dynamic Type settings â it remains fixed.
To work around this we need to create a custom ViewModifier
that can scale up our font size based on the current accessibility setting, and also detect when that setting changes.
Iâm going to give you the code first, then walk through how it works and why:
@available(iOS 13, macCatalyst 13, tvOS 13, watchOS 6, *)
struct ScaledFont: ViewModifier {
@Environment(\.sizeCategory) var sizeCategory
var name: String
var size: Double
func body(content: Content) -> some View {
let scaledSize = UIFontMetrics.default.scaledValue(for: size)
return content.font(.custom(name, size: scaledSize))
}
}
@available(iOS 13, macCatalyst 13, tvOS 13, watchOS 6, *)
extension View {
func scaledFont(name: String, size: Double) -> some View {
return self.modifier(ScaledFont(name: name, size: size))
}
}
Thatâs all the code required to get custom fonts working with Dynamic Type. As an example of using it, hereâs a list with two text views, one using the built-in font and one with a scalable Georgia font:
struct ContentView: View {
var body: some View {
List {
Text("Hello World")
Text("Hello World")
.scaledFont(name: "Georgia", size: 12)
}
}
}
Now youâve seen how it works, letâs look at why it works.
First, we have this custom view modifier:
struct ScaledFont: ViewModifier {
@Environment(\.sizeCategory) var sizeCategory
var name: String
var size: Double
func body(content: Content) -> some View {
let scaledSize = UIFontMetrics.default.scaledValue(for: size)
return content.font(.custom(name, size: scaledSize))
}
}
That accepts a name and size for our font, then uses UIFontMetrics
to scale up the requested font to whatever matches the userâs current device setting, and send it back.
We then wrap that inside an extension on View
to make it easier to use:
@available(iOS 13, macCatalyst 13, tvOS 13, watchOS 6, *)
extension View {
func scaledFont(name: String, size: Double) -> some View {
return self.modifier(ScaledFont(name: name, size: size))
}
}
All that does is wrap up the call to our custom font modifier so that it looks nicer in our views â it means we write .scaledFont(name: "Georgia", size: 12)
to use it, rather than .modifier(ScaledFont(name: "Georgia", size: 12))
.
Now, you might wonder why we need the custom view modifier if all we do is pass on the data. Well, the clue lies in this line in our view modifier:
@Environment(\.sizeCategory) var sizeCategory
That asks the system to provide the current size category from the environment, which determines what level Dynamic Type is set to. The trick is that we donât actually use it â we donât care what the Dynamic Type setting is, but by asking the system to update us when it changes our UIFontMetrics
code will be run at the same time, causing our font to scale correctly.
Tips
The UIFontMetrics
class is not available on macOS, which is why Iâve added the @available
markers.