# App Font Registration Rules Font-registration rules for the planned PureYAML native editor. Register custom fonts using CoreText in SPM packages. Never use Info.plist approaches. Resources must use `.process()` in Package.swift and always use `Bundle.module` for resource access. ## 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?`. - 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 ```swift 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 ```swift 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? 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 ```swift import SwiftUI import AppFont @main struct PureYAMLApp: App { init() { // Register fonts before any UI renders. FontRegistration.registerFonts() } var body: some Scene { WindowGroup { ContentView() } } } ``` ### Font usage in SwiftUI ```swift 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?` - [ ] 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