PureYAML/docs/rules-swift/exp/implementation-patterns.md
Mihaela M 5f3892f2c1 chore(rules): vendor the rules-swift canon into docs/rules-swift
Mirror the shared Swift rules canon (rules-swift, commit 4ebb892) into
docs/rules-swift/ as a read-only reference alongside the repo's tailored
docs/rules/. PROVENANCE.md records the source, commit, and what was
included or excluded; the upstream repo's own VCS, CI, and lint tooling
is omitted as machinery rather than rule content.

Exclude the vendored tree from the style gate (scripts/check-style.sh)
and the pre-commit hook: the canon's enforcement docs quote the
forbidden attribution phrases to define them, the same reason the
repo's own enforcement files are already excluded.
2026-06-26 05:36:12 +02:00

437 lines
12 KiB
Markdown

# Implementation Patterns
Six worked patterns for the package layers used in the canonical Swift monorepo. Foundation, Feature, Middleware, API, Component, Font/Resource.
Cross-referenced from elsewhere:
- Aggregator pattern: see Pattern 5 (Component Layer Architecture) > AllComponents Package, the canonical example
- Font registration anti-patterns: see Pattern 6 (Font/Resource Package) > Why This Pattern
### Pattern 1: Foundation Package (No Dependencies)
```swift
// Packages/Sources/SharedModels/Package.swift (excerpt)
let sharedModelsTarget = Target.target(
name: "SharedModels",
dependencies: [] // RULE: Foundation packages have ZERO dependencies
)
```
```swift
// Packages/Sources/SharedModels/User.swift
public struct User: Identifiable, Codable, Sendable {
public let id: UUID
public let firstName: String
public let lastName: String
public let email: String
public init(id: UUID, firstName: String, lastName: String, email: String) {
self.id = id
self.firstName = firstName
self.lastName = lastName
self.email = email
}
}
```
Foundation packages are pure Swift, no dependencies, highly reusable.
### Pattern 2: Feature Package (Depends on Foundation + Infrastructure)
```swift
// Packages/Sources/AuthFeature/Package.swift (excerpt)
let authFeatureTarget = Target.target(
name: "AuthFeature",
dependencies: [
"SharedModels", // Foundation: domain models
"SharedViews", // Infrastructure: reusable UI
"AppColors", // Foundation: colors
"AppFont", // Foundation: typography
"ApiClient", // Infrastructure: networking
]
)
```
```swift
// Packages/Sources/AuthFeature/LoginView.swift
import SwiftUI
import SharedViews
import SharedModels
import AppColors
import AppFont
public struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
@Environment(\.appColors) var colors
public init() {}
public var body: some View {
VStack {
Text("Welcome")
.bdrFont(.headline) // From AppFont
.foregroundColor(colors.primary) // From AppColors (Apple HIG naming)
// Use shared components from SharedViews
// Use models from SharedModels
}
}
}
```
Features depend on foundations and infrastructure, never on other features (except parent/child relationships).
### Pattern 3: Middleware Package (Single Purpose Infrastructure)
```swift
// Packages/Sources/OpenAPICachingMiddleware/Package.swift (excerpt)
let apiCachingTarget = Target.target(
name: "OpenAPICachingMiddleware",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
]
)
```
Middleware packages are highly focused, single-purpose, minimal dependencies.
### Pattern 4: API Layer Separation
```swift
// RULE: Separate packages for each API concern
// ApiShared: OpenAPI spec + generated DTOs
let apiSharedTarget = Target.target(
name: "ApiShared",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
],
plugins: [
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
]
)
// ApiClient: Client-side networking
let apiClientTarget = Target.target(
name: "ApiClient",
dependencies: [
"ApiShared", // Uses generated DTOs
"SharedModels",
"OpenAPICachingMiddleware",
]
)
// ApiServer: Server-side handlers
let apiServerTarget = Target.target(
name: "ApiServer",
dependencies: [
"ApiShared", // Uses generated DTOs
"SharedModels",
.product(name: "Vapor", package: "vapor"),
]
)
// ApiServerApp: Executable
let apiServerAppTarget = Target.executableTarget(
name: "ApiServerApp",
dependencies: ["ApiServer"]
)
```
API layer has 4 packages: Shared (contract), Client, Server, ServerApp (executable).
### Pattern 5: Component Layer Architecture
The component layer has a strict 3-package hierarchy. Start with the Components package first.
#### Component System Architecture
```
Components (core infrastructure)
↓
SharedComponents (hot reload)
↓
AppComponents (app-specific production components)
```
#### 1. Components Package (CORE - comes FIRST)
**Purpose:** Core component system infrastructure
**Contains:**
- `AnyComponent` - Type-erased component protocol
- `ComponentsBundle` - Bundle management
- `ComponentFactory` - Component instantiation
- `ComponentRegistry` - Global component registration
- `ComponentListComponent` - Component list rendering
- `ComponentRegistrar` - Registration interface
- `SystemComponentRegistrar` - System component registration
- `ComponentListModel` - Component list data model
- `ComponentListView` - Component list view
- `components.json` - Component configuration
**Dependencies:** ZERO (foundation infrastructure)
```swift
// Packages/Package.swift (excerpt)
let componentsTarget = Target.target(
name: "Components",
dependencies: [], // Foundation layer; zero dependencies
resources: [
.process("components.json"),
]
)
```
**Structure:**
```
Packages/Sources/Components/
├── Protocol/
│ ├── AnyComponent.swift
│ └── ComponentRegistrar.swift
├── Registry/
│ ├── ComponentRegistry.swift
│ ├── ComponentFactory.swift
│ └── SystemComponentRegistrar.swift
├── Bundle/
│ └── ComponentsBundle.swift
├── List/
│ ├── ComponentListModel.swift
│ ├── ComponentListView.swift
│ └── ComponentListComponent.swift
└── components.json
```
#### 2. SharedComponents Package (Hot Reload Infrastructure)
**Purpose:** Hot reload and development-time infrastructure
**Dependencies:**
- Inject (hot reload)
- KZFileWatchers (file watching)
```swift
// Packages/Package.swift (excerpt)
let sharedComponentsTarget = Target.target(
name: "SharedComponents",
dependencies: [
.product(name: "Inject", package: "Inject"),
.product(name: "KZFileWatchers", package: "KZFileWatchers"),
]
)
```
**Purpose:** Enables hot reload of components during development
#### 3. AppComponents Package (App-Specific Components)
**Purpose:** Production app-specific components
**Dependencies:**
- Components (core system)
- AppColors (semantic colors with HSV)
- AppFont (typography)
```swift
// Packages/Package.swift (excerpt)
let appComponentsTarget = Target.target(
name: "AppComponents",
dependencies: [
"Components", // Core component system
"AppColors", // Semantic colors (HSV-based)
"AppFont", // App typography
],
resources: [
.process("Resources"), // Images, assets
]
)
```
**Examples:**
- `BenefitCardComponent` - Benefit display card
- `LanguageSwitcherComponent` - Language selection
- `ButtonComponent` - App-specific buttons
**Structure:**
```
Packages/Sources/AppComponents/
├── BenefitCardComponent.swift
├── LanguageSwitcherComponent.swift
├── ButtonComponent.swift
├── Resources/
│ └── Images/
└── AppComponentsRegistration.swift
```
#### 4. AllComponents Package (Aggregator - Optional)
**Purpose:** Umbrella package for convenient imports.
**This is also the canonical example of the general aggregator pattern.** An aggregator package re-exports its dependencies via `@_exported import`, so a consumer who writes `import YourAggregator` gets everything the aggregator depends on. The same shape works for any umbrella package, not just AllComponents (e.g., a hypothetical `AllFeatures` for a preview app would depend on every `*Feature` package and `@_exported import` each one).
**Dependencies:**
- Components
- AppComponents
```swift
// Packages/Package.swift (excerpt)
let allComponentsTarget = Target.target(
name: "AllComponents",
dependencies: [
"Components",
"AppComponents",
]
)
```
```swift
// Packages/Sources/AllComponents/AllComponents.swift
@_exported import Components
@_exported import AppComponents
```
Use only in preview/demo apps and component libraries, NEVER in production feature paths.
#### Component Layer Rules
Rules:
1. Create the Components package first (core infrastructure).
2. SharedComponents depends only on hot reload tools (Inject, KZFileWatchers).
3. AppComponents depends on Components + AppColors + AppFont
4. NEVER skip the Components package - it contains the core system
5. Component configuration goes in `components.json`
6. All components must conform to the protocol defined in Components
7. Registration happens via ComponentRegistry from Components package
**Order of Creation:**
1. Components (AnyComponent, ComponentRegistry, etc.)
2. SharedComponents (hot reload infrastructure)
3. AppComponents (app-specific components)
4. AllComponents (aggregator - optional)
### Pattern 6: Font/Resource Package
Register fonts using CoreText, not via Info.plist. Resources use `.process()` in Package.swift.
#### Package.swift Configuration
```swift
// Packages/Package.swift (excerpt from targets closure)
let appFontTarget = Target.target(
name: "AppFont",
dependencies: [],
resources: [
.process("Fonts"), // .process() registers; .copy() does not
]
)
```
Use `.process("Fonts")` to ensure Bundle.module works correctly. NEVER use `.copy()`.
#### Font Registration Implementation
```swift
// Packages/Sources/AppFont/FontRegistration.swift
import CoreGraphics
import CoreText
import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
public enum FontRegistration {
/// Register custom fonts from the AppFont package
public static func registerFonts() {
// Get all resource URLs and filter for .otf files
guard let resourceURLs = Bundle.module.urls(forResourcesWithExtension: nil, subdirectory: nil) else {
print("⚠️ No resources found in AppFont bundle")
return
}
let fontURLs = resourceURLs.filter { $0.pathExtension.lowercased() == "otf" }
guard !fontURLs.isEmpty else {
print("⚠️ No .otf fonts found in AppFont bundle")
return
}
for url in fontURLs {
var errorRef: Unmanaged<CFError>?
let success = CTFontManagerRegisterFontsForURL(url as CFURL, .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
```
Packages/Sources/AppFont/
├── FontRegistration.swift # Registration using CoreText
├── ScaledFont.swift # Font modifiers (.bdrFont())
└── Fonts/ # Font resources
├── MonitorPro-Normal.otf
├── MonitorPro-Bold.otf
└── MonitorPro-Light.otf
```
#### Usage in App
```swift
// Apps/iosApp/iosAppApp.swift
import SwiftUI
import AppFont
@main
struct iosAppApp: App {
init() {
// Register fonts before any UI renders
FontRegistration.registerFonts()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
#### Why This Pattern?
**Benefits:**
- ✅ Works in SPM packages (Info.plist approach doesn't work in packages)
- ✅ Explicit font registration with error reporting
- ✅ Cross-platform (iOS + macOS) with `#if canImport()` conditionals
- ✅ Uses `Bundle.module` (SPM automatic bundle)
- ✅ Supports multiple font formats (.otf, .ttf) via filter
- ✅ Clear console output showing which fonts loaded
**Why `.process()` not `.copy()`:**
- `.process()` → Resources are processed and accessible via `Bundle.module`
- `.copy()` → Resources copied verbatim, may not work with `Bundle.module`
**Why CoreText not Info.plist:**
- Info.plist font registration only works in app bundles, NOT in SPM packages
- CoreText registration works anywhere (packages, frameworks, apps)
Rules:
1. Use `CTFontManagerRegisterFontsForURL` for font registration.
2. Use `Bundle.module` in SPM packages, not `Bundle.main`.
3. ALWAYS use `.process()` for font resources in Package.swift
4. ALWAYS use `#if canImport(UIKit)` / `#if canImport(AppKit)` for platform imports
5. ALWAYS call `FontRegistration.registerFonts()` in app init BEFORE any UI renders
6. NEVER use Info.plist `UIAppFonts` / `ATSApplicationFontsPath` in packages