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