PureXML/third_party/rules-swift/ui/fonts.md
Mihaela Mihaljevic c7ef8abfd9 chore(rules): pin rules-swift corpus, add gates and CI; drop redundant docs/rules mirror
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.
2026-06-30 14:02:40 +02:00

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 (NOT Bundle.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 with Bundle.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() or AppDelegate.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:

  • UIAppFonts only works in app bundles, NOT packages.
  • ATSApplicationFontsPath only 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 via Bundle.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 with Bundle.module.
  • Using Bundle.main in package code: Bundle.main is for apps, not packages.
  • Skipping error handling on CTFontManagerRegisterFontsForURL.
  • Registering fonts in .onAppear instead of app init: too late, UI already rendered with system fonts.

Checklist

  • Used .process() for resources in Package.swift
  • Created FontRegistration.swift with CoreText registration
  • Imported CoreText and CoreGraphics
  • Used platform-specific imports (#if canImport(UIKit) / #if canImport(AppKit))
  • Used Bundle.module for resource access
  • Filtered font files by extension (.otf, .ttf)
  • Used CTFontManagerRegisterFontsForURL with .process scope
  • 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.main in package code
  • NEVER used .copy() for font resources