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.
12 KiB
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)
// Packages/Sources/SharedModels/Package.swift (excerpt)
let sharedModelsTarget = Target.target(
name: "SharedModels",
dependencies: [] // RULE: Foundation packages have ZERO dependencies
)
// 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)
// 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
]
)
// 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)
// 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
// 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 protocolComponentsBundle- Bundle managementComponentFactory- Component instantiationComponentRegistry- Global component registrationComponentListComponent- Component list renderingComponentRegistrar- Registration interfaceSystemComponentRegistrar- System component registrationComponentListModel- Component list data modelComponentListView- Component list viewcomponents.json- Component configuration
Dependencies: ZERO (foundation infrastructure)
// 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)
// 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)
// 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 cardLanguageSwitcherComponent- Language selectionButtonComponent- 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
// Packages/Package.swift (excerpt)
let allComponentsTarget = Target.target(
name: "AllComponents",
dependencies: [
"Components",
"AppComponents",
]
)
// 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:
- Create the Components package first (core infrastructure).
- SharedComponents depends only on hot reload tools (Inject, KZFileWatchers).
- AppComponents depends on Components + AppColors + AppFont
- NEVER skip the Components package - it contains the core system
- Component configuration goes in
components.json - All components must conform to the protocol defined in Components
- Registration happens via ComponentRegistry from Components package
Order of Creation:
- Components (AnyComponent, ComponentRegistry, etc.)
- SharedComponents (hot reload infrastructure)
- AppComponents (app-specific components)
- 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
// 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
// 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
// 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 viaBundle.module.copy()→ Resources copied verbatim, may not work withBundle.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:
- Use
CTFontManagerRegisterFontsForURLfor font registration. - Use
Bundle.modulein SPM packages, notBundle.main. - ALWAYS use
.process()for font resources in Package.swift - ALWAYS use
#if canImport(UIKit)/#if canImport(AppKit)for platform imports - ALWAYS call
FontRegistration.registerFonts()in app init BEFORE any UI renders - NEVER use Info.plist
UIAppFonts/ATSApplicationFontsPathin packages