Adopt the rules-swift pin-and-codify strategy in snapshot mode (third_party/rules-swift @97dc70f + sha256), add codified gates, a conformance audit ledger, and .github/workflows/rules-swift.yml (corpus-pin integrity required, full gate report mode). Remove the docs/rules mirror it supersedes (repo-specific files kept under docs/). Style and leak gates exclude the vendored corpus.
6.7 KiB
6.7 KiB
Fonts
How to register custom fonts in a Tiledown-style native editor: CoreText registration inside SPM packages, never the Info.plist approaches. Resources must use .process() in Package.swift, and resource access always goes through Bundle.module.
Core rules
Rule 1: CoreText registration
Use CTFontManagerRegisterFontsForURL for font registration:
- MUST import CoreText and CoreGraphics.
- MUST use
Bundle.module(NOTBundle.main). - MUST handle errors with
Unmanaged<CFError>?. - MUST provide console logging for success/failure.
- MUST filter font files by extension (.otf, .ttf).
Rule 2: Package.swift configuration
Use .process() for font resources:
- NEVER use
.copy(). It will not work withBundle.module. - MUST organize fonts in a dedicated subdirectory (e.g.
Fonts/). - The package MUST have zero dependencies (foundation layer).
Rule 3: Platform imports
Use platform-specific imports:
#if canImport(UIKit)for iOS.#elseif canImport(AppKit)for macOS.- Import UIKit/AppKit for platform types.
Rule 4: App initialization
Register fonts in app init before UI renders:
- Call in
App.init()orAppDelegate.applicationDidFinishLaunching. - Registration is synchronous and must complete before SwiftUI renders.
Rule 5: Never use Info.plist
Do not use Info.plist font registration in SPM packages:
UIAppFontsonly works in app bundles, NOT packages.ATSApplicationFontsPathonly works in app bundles.- CoreText is the ONLY approach that works in SPM packages.
Implementation pattern
Package.swift configuration
let appFontTarget = Target.target(
name: "AppFont",
dependencies: [], // Foundation layer; zero dependencies
resources: [
.process("Fonts"), // MUST be .process(); .copy() silently fails to register fonts
]
)
Why .process() not .copy():
.process(): resources processed and accessible viaBundle.module..copy(): resources copied verbatim, may not work correctly.
Font registration implementation
import CoreGraphics
import CoreText
import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
public enum FontRegistration {
/// Register custom fonts shipped with the AppFont package.
public static func registerFonts() {
// Use Bundle.module for SPM packages.
guard let resourceURLs = Bundle.module.urls(
forResourcesWithExtension: nil,
subdirectory: nil
) else {
print("No resources found in AppFont bundle")
return
}
// Filter by font extension (.otf, .ttf).
let fontURLs = resourceURLs.filter { url in
let ext = url.pathExtension.lowercased()
return ext == "otf" || ext == "ttf"
}
guard !fontURLs.isEmpty else {
print("No font files found in AppFont bundle")
return
}
// Register each font with error handling.
for url in fontURLs {
var errorRef: Unmanaged<CFError>?
let success = CTFontManagerRegisterFontsForURL(
url as CFURL,
.process, // Register for the current process
&errorRef
)
if !success {
print("Failed to register font: \(url.lastPathComponent)")
if let error = errorRef?.takeRetainedValue() {
print(" Error: \(error)")
}
} else {
print("Registered font: \(url.lastPathComponent)")
}
}
}
}
Package directory structure
Sources/AppFont/
├── FontRegistration.swift # CoreText registration (this file)
├── ScaledFont.swift # Font modifiers (e.g. .appFont())
├── FontStyles.swift # Font style definitions
└── Fonts/ # Font resources
├── EditorSans-Regular.otf
├── EditorSans-Bold.otf
├── EditorSans-Light.otf
└── EditorSans-Italic.otf
App initialization
import SwiftUI
import AppFont
@main
struct TiledownApp: App {
init() {
// Register fonts before any UI renders.
FontRegistration.registerFonts()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Font usage in SwiftUI
import SwiftUI
import AppFont
struct MyView: View {
var body: some View {
VStack {
Text("Headline")
.appFont(.headline) // Custom font with Dynamic Type
Text("Body text")
.appFont(.body)
}
}
}
Why this pattern?
- Works in SPM packages. Info.plist approaches only work in app bundles; CoreText registration works in packages, frameworks, and apps.
- Explicit error reporting. Console logs show which fonts loaded and why a failure happened.
- Cross-platform. Works on iOS and macOS with conditional imports, same code path.
- Uses
Bundle.module. SPM manages the bundle; no manual path handling. - Multiple formats.
.otf(recommended) and.ttf, filtered by extension.
CoreText vs Info.plist
| Approach | Works in app bundle | Works in SPM package |
|---|---|---|
Info.plist (UIAppFonts) |
Yes | No |
Info.plist (ATSApplicationFontsPath) |
Yes | No |
CoreText (CTFontManagerRegisterFontsForURL) |
Yes | Yes |
CoreText is the only approach that works universally.
Common mistakes
- Using Info.plist (
UIAppFonts/ATSApplicationFontsPath) in a package: does not work. - Using
.copy()for resources: will not work withBundle.module. - Using
Bundle.mainin package code:Bundle.mainis for apps, not packages. - Skipping error handling on
CTFontManagerRegisterFontsForURL. - Registering fonts in
.onAppearinstead of app init: too late, UI already rendered with system fonts.
Checklist
- Used
.process()for resources in Package.swift - Created
FontRegistration.swiftwith CoreText registration - Imported CoreText and CoreGraphics
- Used platform-specific imports (
#if canImport(UIKit)/#if canImport(AppKit)) - Used
Bundle.modulefor resource access - Filtered font files by extension (.otf, .ttf)
- Used
CTFontManagerRegisterFontsForURLwith.processscope - Implemented error handling with
Unmanaged<CFError>? - Added console logging for success/failure
- Called registration in app init BEFORE UI renders
- Fonts organized in a
Fonts/subdirectory - Package has zero dependencies
- NEVER used Info.plist (
UIAppFonts,ATSApplicationFontsPath) - NEVER used
Bundle.mainin package code - NEVER used
.copy()for font resources