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.
437 lines
12 KiB
Markdown
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
|
|
|