Docs · Apple
Font controls in an app
A SwiftUI slider that redraws a word as it moves, real text in the font a moment later, and a font your users can keep. Each step is a whole view you can paste into your app.
Add font controls explains the ideas behind this page: few controls, outlines while dragging, the newest change wins, and a recipe to store. Here they are in Swift.
1. One engine for the whole app
There's nothing to load: the framework is part of your app. Make one TypomatEngine and hand it to the views that need it. It's Sendable, so any view or task can use it.
import SwiftUI
import TypomatEngine
@main
struct AcmeApp: App {
// One engine for the whole app. It's Sendable, so any view or task can use it.
let engine = TypomatEngine(licenseKey: "TM2...")
var body: some Scene {
WindowGroup {
FontDial(engine: engine, text: "Acme")
}
}
}2. Draw the word while the slider moves
Outlines take about a millisecond a letter, and glyphs is safe to call on any thread, so a SwiftUI Canvas can draw them on every change. fitted: false skips the curve fitting, for speed. The paths are in font units, 1000 to the em, so the view scales them down to a tenth.
import SwiftUI
import TypomatEngine
struct FontDial: View {
let engine: TypomatEngine
let text: String
@State private var weight = 118.0
var body: some View {
Canvas { context, _ in
let recipe = Recipe(parameters: [
"weight": weight, "contrast": 0.92,
"serif": 0.5, "xHeight": 470,
])
guard let run = try? engine.glyphs(
text, recipe: recipe, fitted: false
) else { return }
context.scaleBy(x: 0.1, y: 0.1)
context.translateBy(x: 0, y: run.lineTop)
for glyph in run.glyphs where !glyph.isMissing {
context.fill(Path(glyph.path), with: .foreground)
context.translateBy(
x: glyph.advance + glyph.kerning, y: 0
)
}
}
Slider(value: $weight, in: 12...210) // Your slider.
}
}What you should see: the word, getting heavier and lighter as you drag. The slider's range here is the weight's own. Read it from the engine rather than writing it down, and it stays right when the engine changes:
// The slider's range, from the engine rather than from your code.
let weightAxis = try engine.catalog().axes.first { $0.id == "weight" }!
// Then, in your view:
Slider(value: $weight, in: weightAxis.minimum...weightAxis.maximum)
// weightAxis.defaultValue is where the standard font has it: 86.3. Set real text in the font
To set real text with Text, compile a font of only the characters on screen, far quicker than all 271, and register it. .task(id:) does the rest: when the recipe changes, SwiftUI cancels the task before and starts a new one, so only the newest weight is ever shown.
import SwiftUI
import TypomatEngine
struct LiveHeading: View {
let engine: TypomatEngine
let text: String
@State private var weight = 118.0
@State private var shown: RegisteredFont?
var recipe: Recipe {
Recipe(name: "Preview (Int(weight))", parameters: ["weight": weight, "contrast": 0.92])
}
var body: some View {
VStack {
Text(text)
.font(shown.map { .custom($0.postScriptName, size: 48) } ?? .largeTitle)
Slider(value: $weight, in: 12...210, step: 1)
}
// A new recipe cancels the task before, so only the newest weight is registered.
.task(id: recipe) {
guard let data = try? await engine.compile(recipe, format: .ttf, characters: .characters(text)),
let font = try? await TypomatEngine.registerFont(data) else { return }
await shown?.unregister()
shown = font
}
}
}Each weight gets its own font name. A SwiftUI view may keep showing a font it already has until it asks for it again, so a new name is what makes the text change at once. The earlier font is unregistered when the new one is in. The heaviest named styles take far longer than the rest, up to a few seconds, so keep the outlines on screen while they finish.
4. Save a recipe, not the file
A Recipe is the font's source, about a kilobyte of JSON, and it compiles to the same bytes whenever you ask. It's Codable, so store it with the rest of your data, and compile when you need the file.
let stored = try JSONEncoder().encode(recipe) // about a kilobyte
let again = try JSONDecoder().decode(Recipe.self, from: stored)
let font = try await engine.compile(again, format: .woff2)5. Let them keep it
compile returns a complete file. Write it out and hand it to a share sheet, and people can save it to Files, AirDrop it, or install it. Fonts your users make are theirs to use commercially: see Licensing.
let ttf = try await engine.compile(recipe, format: .ttf)
let file = FileManager.default.temporaryDirectory.appendingPathComponent("\(recipe.name).ttf")
try ttf.write(to: file)
// Then, in SwiftUI: ShareLink(item: file)