222 lines
6.7 KiB
Markdown
222 lines
6.7 KiB
Markdown
# 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<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
|
|
|
|
```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<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
|
|
|
|
```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<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
|