467 lines
14 KiB
Markdown
467 lines
14 KiB
Markdown
# Implementation Patterns
|
|
|
|
Six worked patterns for the target 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 target, the canonical example
|
|
- Font registration anti-patterns: see Pattern 6 (Font/Resource target) > Why This Pattern
|
|
|
|
### Pattern 1: Foundation Target (No Dependencies)
|
|
|
|
```swift
|
|
// Packages/Package.swift (SharedModels excerpt)
|
|
let sharedModelsTarget = Target.target(
|
|
name: "SharedModels",
|
|
dependencies: [] // RULE: Foundation targets 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 targets are pure Swift, have no dependencies, and are highly reusable.
|
|
|
|
### Pattern 2: Feature Target (Depends on Foundation + Infrastructure)
|
|
|
|
```swift
|
|
// Packages/Package.swift (AuthFeature 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 Target (Single Purpose Infrastructure)
|
|
|
|
```swift
|
|
// Packages/Package.swift (OpenAPICachingMiddleware excerpt)
|
|
let apiCachingTarget = Target.target(
|
|
name: "OpenAPICachingMiddleware",
|
|
dependencies: [
|
|
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
|
|
]
|
|
)
|
|
```
|
|
|
|
Middleware targets are highly focused, single-purpose, and have minimal dependencies.
|
|
|
|
### Pattern 4: API Layer Separation
|
|
|
|
```swift
|
|
// RULE: Separate targets 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 four targets: Shared (contract), Client, Server, and ServerApp (executable).
|
|
|
|
### Pattern 5: Component Layer Architecture
|
|
|
|
The component layer has a strict three-target hierarchy. Start with the Components target first.
|
|
|
|
#### Component System Architecture
|
|
|
|
```
|
|
Components (core infrastructure)
|
|
↓
|
|
SharedComponents (hot reload)
|
|
↓
|
|
AppComponents (app-specific production components)
|
|
```
|
|
|
|
#### 1. Components Target (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 Target (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 Target (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 Target (Aggregator - Optional)
|
|
|
|
**Purpose:** Umbrella target for convenient imports.
|
|
|
|
**This is also the canonical example of the general aggregator pattern.** An
|
|
aggregator target re-exports its target dependencies via `@_exported import`, so
|
|
a consumer who writes `import YourAggregator` gets every module the aggregator
|
|
depends on. The same shape works for any umbrella target, not just AllComponents
|
|
(for example, a hypothetical `AllFeatures` preview target would depend on every
|
|
`*Feature` target 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 target 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 target - 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 the Components target
|
|
|
|
**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 Target
|
|
|
|
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 target
|
|
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)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Target 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 SwiftPM targets (the Info.plist approach does not)
|
|
- ✅ 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 SwiftPM library targets
|
|
- CoreText registration works anywhere (SwiftPM targets, frameworks, apps)
|
|
|
|
Rules:
|
|
1. Use `CTFontManagerRegisterFontsForURL` for font registration.
|
|
2. Use `Bundle.module` in SwiftPM targets, 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 SwiftPM targets
|
|
|
|
## Acceptance check
|
|
|
|
The patterns are conforming when their checkable invariants hold:
|
|
|
|
- **Foundation and core-infrastructure targets carry zero dependencies.** The `SharedModels` (Pattern 1) and `Components` (Pattern 5) targets declare `dependencies: []`. Confirm by reading those target definitions in `Packages/Package.swift`; a non-empty list is a violation.
|
|
|
|
- **Fonts register via CoreText against `Bundle.module`, never Info.plist or `Bundle.main`** (Pattern 6):
|
|
|
|
```bash
|
|
grep -rnE 'UIAppFonts|ATSApplicationFontsPath|\.copy\("Fonts"\)|Bundle\.main' Packages/Sources/AppFont/
|
|
grep -rn 'CTFontManagerRegisterFontsForURL' Packages/Sources/AppFont/
|
|
```
|
|
|
|
The first MUST be empty; the second MUST be non-empty. Font resources use `.process("Fonts")` and `FontRegistration.registerFonts()` is called in app init before any UI renders.
|
|
|
|
- **The aggregator is confined to preview/demo.** An `@_exported import` umbrella (Pattern 5, AllComponents) is imported only from preview/demo apps and component libraries, never from a production feature path:
|
|
|
|
```bash
|
|
grep -rln '@_exported import' Packages/Sources/
|
|
```
|
|
|
|
Each hit MUST be an aggregator target used only off the production path.
|
|
|
|
- **Observable checklist** for the structural shape, each a clear pass/fail: the API layer keeps its four-target split (Shared contract, Client, Server, ServerApp executable, Pattern 4); features depend on foundations and infrastructure but never on sibling features except a declared parent/child (Pattern 2); middleware targets stay single-purpose with minimal dependencies (Pattern 3); and the component layer is created in order Components, SharedComponents, AppComponents, AllComponents with the Components target never skipped (Pattern 5).
|