Docs · Apple

Swift SDK

TypomatEngine is the Typomat engine as a native framework for iOS 17, macOS 14 and Mac Catalyst 17 or later. It makes fonts on the device, with Swift types, no dependencies and no network use.

This page lists everything the framework does. If you haven't added it yet, the Apple quickstart does that in a few minutes. Ideas the two SDKs share, such as recipes and character sets, are explained once in SDK concepts.

Installing

In Xcode, choose File, then Add Package Dependencies, and enter https://github.com/MarcoLuthi/typomat-engine-open. In a Package.swift:

Swift
.package(url: "https://github.com/MarcoLuthi/typomat-engine-open", from: "1.7.1")

and .product(name: "TypomatEngine", package: "typomat-engine-open") in your target's dependencies. It's a dynamic framework, 2.8 MB on an iPhone, which an app embeds by itself. Package.resolved pins the version you build with, so a build keeps working after a licence ends.

TypomatEngine(licenseKey:)

Makes the engine. Make one, and share it: it holds nothing between calls, and any method can be called from any thread. The key is read once, on the device, together with your app's bundle ID. Leave the key out, or pass nil, for evaluation mode.

Swift
import TypomatEngine

// From your account. Older TM1 keys work too.
let engine = TypomatEngine(licenseKey: "TM2...")

compile(_:format:characters:weightClass:)

Makes a whole font from a recipe and gives back the file, as Data. It's async: the work runs on a thread of its own, away from the caller, so it's safe to await from the main actor.

Swift
let recipe = Recipe(name: "Acme Display", parameters: ["weight": 118, "contrast": 0.92])

let font = try await engine.compile(recipe, format: .woff2, characters: .sets("lowercase", "digits"))
ArgumentMeaning
recipeThe font's source: its name, parameters, metadata and family. Recipe(), the standard font, if you leave it out.
formatA FontFormat: .ttf by default, or .otf, .woff or .woff2. Its fileExtension names the file.
charactersA CharacterSelection: .all by default. See Characters.
weightClassWhich member of the recipe's family to compile: 100, 200, and so on up to 900.

Every parameter is optional: anything left out stays at its default, and values outside a range are clamped. An unknown name throws. See all the parameters. Cancelling the task is noticed before the work starts and when it ends, not during it.

Characters

A font holds all 271 characters unless you pick. Pick to make it much smaller. A string is a selection by itself, and the others combine: a font holds whatever any part asks for, as far as the engine can draw it, and always a space. The set names are in SDK concepts.

Swift
try await engine.compile(recipe, characters: "0123456789")           // only these
try await engine.compile(recipe, characters: .sets("lowercase", "digits"))
try await engine.compile(recipe, characters: .ranges(0x41...0x5A))   // A to Z

// All three at once: a font holds whatever any part asks for.
let mix = CharacterSelection(characters: "€", sets: ["digits"], ranges: [0x41...0x5A])
try await engine.compile(recipe, format: .woff2, characters: mix)

Using a font straight away

A compiled font can go straight into your interface, with nothing installed. TypomatEngine.registerFont(_:) makes it available to this app by name, and gives back a RegisteredFont with its postScriptName and familyName.

Swift
let data = try await engine.compile(recipe)

// In this app, until it quits or you call unregister().
let font = try await TypomatEngine.registerFont(data)

Text("Hello").font(.custom(font.postScriptName, size: 32))

It lasts until the app quits or you call unregister(). UIFont(name: font.postScriptName, size: 24) works too. Registering under a name you've already used replaces the font, but a SwiftUI view may keep showing the earlier one until it asks for the font again. Give each look its own recipe name if a view must change at once.

To keep the file, write it out, or hand it to a share sheet:

Swift
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)

coverage(_:recipe:)

What a font would hold for a selection, and what the engine can't draw, without compiling anything.

Swift
let coverage = try engine.coverage("Aé€Ж")
// coverage.included: [" ", "A", "é", "€"], coverage.unavailable: ["Ж"]

included is what the font would hold. unavailable is what you asked for and the engine can't draw: 256 at most, lowest code point first. withheld is what it could draw but holds back in evaluation mode, and sets has each set's characters for this recipe.

Recipes

A Recipe is a font's source, in about a kilobyte of JSON: store it rather than the file, and compile it again whenever you need the file. It's Codable and Hashable. SDK concepts explains what's in one.

Swift
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)

Recipe(name:parameters:) covers most uses. name is the family name people see in menus, up to 64 characters: "Typomat Font" if you leave it out. parameters is a [String: Double] by parameter name. metadata is what the file says about itself, and changes no letter:

Swift
var recipe = Recipe(name: "Acme Display", parameters: ["weight": 118])
recipe.metadata.designer = "Acme"
recipe.metadata.copyright = "© 2026 Acme AG"
recipe.metadata.embedding = .installable

The other fields, weights, pulls, points, letters and letterStyles, hold a family and hand edits. If your app has a type of its own called Recipe, reach this one as TypomatEngine::Recipe, which takes Swift 6.3 or later.

family(_:)

A recipe's weights make a family: the font itself, plus one member for each weight you add. family lists them, lightest first, and compile makes each by its weightClass.

Swift
var recipe = Recipe(name: "Acme", parameters: ["weight": 118])
recipe.weights = [.init(weightClass: 700)]

let family = try engine.family(recipe)

for member in family.members {
    let font = try await engine.compile(recipe, weightClass: member.weightClass)
    print(member.style, font.count)   // Medium, then Bold
}

glyphs(_:recipe:fitted:)

Outlines for a line of text, for drawing letters yourself without registering a font. Each glyph has a path, a CGPath in font units (1000 to the em) with y pointing down, the way UIKit and SwiftUI draw, and the same outline as SVG path data in svgPath.

Swift
let run = try engine.glyphs("Acme", recipe: Recipe(parameters: ["weight": 170]))

for glyph in run.glyphs where !glyph.isMissing {
    // glyph.path is a CGPath, and glyph.svgPath the same outline as SVG path data.
    print(glyph.character, glyph.advance, glyph.kerning)
}

Move along by advance + kerning after each glyph. The run's lineTop and lineBottom give the line box, counted upward from the baseline. A glyph the engine can't draw has isMissing set, and isWithheld too when it's held back in evaluation mode. By default the outlines go through the font's own curve fitter, so they match the compiled font. Pass fitted: false for faster, polygonal outlines in a small live preview. Text can be up to 16,384 bytes.

outline(_:recipe:steps:fitted:)

One letter, with the touch points of its anatomy in anchors, by parameter name. The Typomat app uses these to put its dials on a letter, and you could too. Unlike glyphs, the path has y pointing up, as in a font file, so flip it to draw it in UIKit or SwiftUI.

Swift
let outline = try engine.outline("a", recipe: recipe)

outline.path      // a CGPath, y up, as in a font file
outline.advance   // 660.784
outline.anchors   // ["aperture": ..., "contrast": ..., ...]

steps sets how finely curves are cut into lines, from 2 to 64: 24 by default. fitted is false by default here, for raw polygons.

metrics(_:)

A recipe's vertical measurements in font units: ascender, descender, capHeight, lineTop, lineBottom and fitWidth. And lines: a few sample sentences that suit the font's character.

Swift
let metrics = try engine.metrics(recipe)

metrics.ascender    // 740
metrics.capHeight   // 700
metrics.lines       // sample sentences that suit the font

catalog()

Everything this engine can do, from the engine itself, so your interface never carries a copy that goes stale. It never changes while the app runs. SDK concepts says what each part is for.

Swift
let catalog = try engine.catalog()

catalog.axes            // id: "weight", group: "stroke", minimum: 12, maximum: 210, defaultValue: 86, tier: .core, ...
catalog.groups          // ["form", "stroke", "ends", "flourish", "carve", "style"]
catalog.characters      // ["a", "b", "c", ...]
catalog.characterSets   // ["lowercase": [...], "digits": [...], ...]
catalog.forms           // which parameter swaps a letter for its other form
catalog.weightClasses   // [100, 200, ..., 900]

engine is the engine's version, and alternates lists every form each letter has. presets is the same list as below.

presets()

The 34 named styles, from Didone to Blackletter. Each has a name, its parameters, isBeta for one still being refined, and a ready .recipe.

Swift
let presets = try engine.presets()   // 34 named styles

let font = try await engine.compile(presets[0].recipe)

license

What the engine made of your key. mode is .licensed, .lapsed or .evaluation, and reason says why when it isn't licensed. A key that checked out also gives its licensee, plan, domains, apps, expires and updatesUntil.

Swift
let engine = TypomatEngine(licenseKey: "TM2...")

engine.license.mode       // .licensed, .lapsed or .evaluation
engine.license.reason     // why, when it isn't licensed: .app means your key doesn't name this app
engine.license.licensee   // "Acme AG"

A reason of .app means your key doesn't name this app. Licensing has the rules, and nothing about a licence ever throws.

Threads and concurrency

TypomatEngine is Sendable, and so is everything it gives back. compile is async and runs on its own thread, because a whole font takes about 125 ms. Everything else is synchronous and safe on any thread: an outline takes about a millisecond in most styles, so glyphs can follow a slider directly.

Swift
// Fast enough to call on every change, from any thread.
let run = try engine.glyphs(word, recipe: recipe, fitted: false)
drawPreview(run.glyphs)

To compile while something keeps changing, let SwiftUI's .task(id:) do the work: a new value cancels the task before, so the newest change wins.

Swift
// In a SwiftUI view. A new recipe cancels the task, so the newest change wins.
.task(id: recipe) {
    var look = recipe
    look.name = "Preview \(abs(recipe.hashValue))"   // A new name, so the view changes at once.
    guard let data = try? await engine.compile(look, characters: .characters(word)),
          let font = try? await TypomatEngine.registerFont(data) else { return }
    await shown?.unregister()
    shown = font
}

Font controls in an app puts these together in a whole view.

Errors

Everything that can fail throws a TypomatError, with a code to branch on and a message for your logs. code is a struct rather than an enum, so a code this version has no name for still arrives, in code.rawValue.

Swift
do {
    _ = try await engine.compile(Recipe(parameters: ["wieght": 118]))
} catch let error as TypomatError where error.code == .unknownParameter {
    print(error.message)   // There's no parameter called "wieght".
}
CoderawValueMeaning
.unknownParameterunknown_parameterA recipe names a parameter the engine doesn't have, so a typo can't silently do nothing.
.invalidRequestinvalid_requestA recipe or request the engine can't use. The message says which part.
.unknownSetunknown_setA character selection names a set that doesn't exist.
.newerFormatnewer_formatA recipe from a newer engine than this one.
.fontRegistrationfont_registrationThe system wouldn't take a font you registered: it can't read it, or its name is taken.
.internalinternalA bug in the engine. Please tell us what you sent it.

A code added in a later version arrives with its own rawValue, so keep a default case when you switch on code.