Using custom fonts with SwiftUI

ยท 1175 words ยท 6 minute read

Apple’s documentation describes how to use custom fonts with SwiftUI, but it misses a few vital bits of detail.

Let’s add a custom font to an app, and explore how the .fontWeight() and .italic() modifiers work.

Add a custom font to your Xcode project ๐Ÿ”—

I’m using Barlow for this blogpost. You can get the full set of .ttf (TrueType font) files by hitting the “Download Barlow” button on Jeremy Tribby’s website.

The download contains 54 fonts, but I’ll just use the non-condensed version. This comprises 18 font files: nine weights in both normal and italic variants. I made a new folder in Xcode called Barlow, and dragged those 18 fonts into that Xcode folder. Be sure to check “Copy items if needed” checkbox, and add the files to your target.

No Info.plist file in Xcode? ๐Ÿ”—

If your project was created with Xcode 13 or later, it might not have an Info.plist file. To create one:

  • select your project in the Project navigator
  • select your target
  • select the new “Info” tab at the top, to view a pseudo Info.plist file
  • when you add extra items to this fake plist, then a real Info.plist will be created and added to your project

Add the fonts to Info.plist, so your app can use them ๐Ÿ”—

Add a new item to Info.plist called “Fonts provided by application” (or UIAppFonts if you’re editing the plist as XML). The value for this item is an array of strings - one string for each TrueType font file.

Fonts in the plist must be the full path in the app bundle, including the .ttf file extension.

I’ll start by adding the “Regular” and “Bold” fonts - we’ll do more later. Here’s my Info.plist file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>UIAppFonts</key>
	<array>
		<string>Barlow/Barlow-Regular.ttf</string>
		<string>Barlow/Barlow-Bold.ttf</string>
	</array>
</dict>
</plist>

Using the custom font ๐Ÿ”—

Use the .font(.custom(_, size:)) view modifier to set the font, specifying the name of the font to use. If you ask for a name which doesn’t exist, then the default system font will be used. I’ve used size: 17 here, to match the standard SwiftUI body size.

// there's no `.bold()` or `.fontWeight()` modifier,
// so the default weight will be used
Text("Hello, world!")
  .font(.custom("Barlow", size: 17))

When using the Font.custom(_, size:) function, you may specify either the family name or the PostScript name. If you use the family name (Barlow), then that font family will be used. If you use a PostScript name (eg. Barlow-SemiBold) this will also change the default weight - the weight used when there is no fontWeight() view modifier. You can open the font file in the macOS “Font Book” app to discover the family name and PostScript name.

The TrueType font filename (eg. Barlow-BoldItalic.ttf) usually contains the family name, weight and italic style - but this is done to help you, the developer, not the system. SwiftUI does not use the filename when deciding which TrueType font file to use, and there’s no requirement for the filename to follow any pattern.

You may add a .bold() or .fontWeight(.bold) view modifier to use a heavier weight:

// This will use "Barlow-Bold.ttf"
Text("Hello, world!")
  .font(.custom("Barlow", size: 17))
  .bold()

Selecting the correct TrueType font ๐Ÿ”—

The default weight is .regular - this will be used if there’s no .bold() or .fontWeight() view modifier.

If a weight is specified, then SwiftUI will try to find a font whose weight is closest to the one requested.

How does SwiftUI find the correct font weight? ๐Ÿ”—

There are a set of weight symbols defined in SwiftUI’s Font.Weight struct. Each of these maps to a normalised weight value between -1.0 and 1.0, where .regular is 0.

We can discover SwiftUI’s normalised weights like this:

print("SwiftUI standard weights:")
let allWeights: [Font.Weight] = [
	.ultraLight,
	.thin,
	.light,
	.regular,
	.medium,
	.semibold,
	.bold,
	.heavy,
	.black
]
for weight in allWeights {
	dump(weight)
}

That reveals this set of weights that SwiftUI will look for:

SwiftUI.Font.WeightNormalised weight
.ultraLight-0.8
.thin-0.6
.light-0.4
.regular0
.medium0.23
.semibold0.3
.bold0.4
.heavy0.56
.black0.62

The TrueType font files also contain a weight; we can look inside each file in the Barlow family to find its weight:

// find all fonts in Info.plist with the family name "Barlow"
let barlowFonts = UIFont.fontNames(forFamilyName: "Barlow")
for fontName in barlowFonts {
	let font = UIFont(name: fontName, size: 17)!
	if let traits = font.fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any],
	   let weightValue = traits[.weight] {
		print("\(font.fontDescriptor.postscriptName) has weight: \(weightValue)")
	}
}

This gives us a set of weights for our Barlow TrueType fonts:

PostScript nameNormalised weight
Barlow-Thin-0.315
Barlow-ExtraLight-0.273
Barlow-Light-0.230
Barlow-Regular0
Barlow-Medium0.200
Barlow-SemiBold0.300
Barlow-Bold0.400
Barlow-ExtraBold0.600
Barlow-Black0.800

Knowing that SwiftUI searches for the closest weight, we can map SwiftUI weights to Barlow fonts:

SwiftUI.Font.WeightNormalised weightClosest Barlow fontClosest Barlow font weight
.ultraLight-0.8Barlow-Thin-0.315
.thin-0.6Barlow-Thin-0.315
.light-0.4Barlow-Thin-0.315
.regular0Barlow-Regular0
.medium0.23Barlow-Medium0.2
.semibold0.3Barlow-SemiBold0.3
.bold0.4Barlow-Bold0.4
.heavy0.56Barlow-ExtraBold0.6
.black0.62Barlow-ExtraBold0.6

You can see that multiple SwiftUI weights map to the same Barlow font - and it’s not possible to display “Barlow-ExtraLight”, “Barlow-Light” or “Barlow-Black” by using the SwiftUI .fontWeight() view modifier.

I wonder if it’s possible to get all the SwiftUI weights and TrueType fonts working together somehow? Maybe by loading the TrueType files manually, hacking their weight, and then adding them to the runtime using CoreText? (This StackOverflow question shows how to add files in code, instead of using Info.plist.)

Finding italic fonts ๐Ÿ”—

TrueType fonts also contain a set of symbolic traits, which map to values in UIFontDescriptor.SymbolicTraits. SwiftUI looks for fonts with the symbolic trait .traitItalic.

We can find the numerical representation of these symbolic traits for a TrueType font like this:

let barlowFonts = UIFont.fontNames(forFamilyName: "Barlow")
print("Barlow symbolic traits")
for fontName in barlowFonts {
	let font = UIFont(name: fontName, size: 17)!
	print("traits: \(font.fontDescriptor.symbolicTraits)")
}

Then these numbers can be mapped to the SymbolicTraits option-set in a bitwise way - where 1 is .traitItalic, 2 is .traitBold, 8 is .traitCondensed, etc.

Adding the remaining usable fonts ๐Ÿ”—

Now we know which Barlow fonts can be easily used with SwiftUI, we can add them to Info.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>UIAppFonts</key>
	<array>
		<string>Barlow/Barlow-Thin.ttf</string>
		<string>Barlow/Barlow-ThinItalic.ttf</string>
		<string>Barlow/Barlow-Regular.ttf</string>
		<string>Barlow/Barlow-Italic.ttf</string>
		<string>Barlow/Barlow-Medium.ttf</string>
		<string>Barlow/Barlow-MediumItalic.ttf</string>
		<string>Barlow/Barlow-SemiBold.ttf</string>
		<string>Barlow/Barlow-SemiBoldItalic.ttf</string>
		<string>Barlow/Barlow-Bold.ttf</string>
		<string>Barlow/Barlow-BoldItalic.ttf</string>
		<string>Barlow/Barlow-ExtraBold.ttf</string>
		<string>Barlow/Barlow-ExtraBoldItalic.ttf</string>
	</array>
</dict>
</plist>

Setting a default font family in SwiftUI ๐Ÿ”—

The last thing to do is set our default font family (and maybe a default weight) for the whole SwiftUI app.

This can be done by setting an environment value, with the .environment() view modifier:

// set default font family
.environment(\.font, .custom("Barlow", size: 17))

or:

// set default font family and weight
.environment(\.font, .custom("Barlow-SemiBold", size: 17))

I’d probably do it using a dedicated view modifier, like this:

private struct ThemeFontViewModifier: ViewModifier {
	func body(content: Content) -> some View {
		content
			.environment(\.font, .custom("Barlow", size: 17))
	}
}

extension View {
	func themeFont() -> some View {
		self
			.modifier(ThemeFontViewModifier())
	}
}