14 KiB
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)
// Packages/Package.swift (SharedModels excerpt)
let sharedModelsTarget = Target.target(
name: "SharedModels",
dependencies: [] // RULE: Foundation targets 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 targets are pure Swift, have no dependencies, and are highly reusable.
Pattern 2: Feature Target (Depends on Foundation + Infrastructure)
// 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
]
)
// 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)
// 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
// 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 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 Target (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 Target (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 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
// 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 target first (core infrastructure).
- SharedComponents depends only on hot reload tools (Inject, KZFileWatchers).
- AppComponents depends on Components + AppColors + AppFont
- NEVER skip the Components target - 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 the Components target
Order of Creation:
- Components (AnyComponent, ComponentRegistry, etc.)
- SharedComponents (hot reload infrastructure)
- AppComponents (app-specific components)
- 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
// 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 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
// 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 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 SwiftPM library targets
- CoreText registration works anywhere (SwiftPM targets, frameworks, apps)
Rules:
- Use
CTFontManagerRegisterFontsForURLfor font registration. - Use
Bundle.modulein SwiftPM targets, 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 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) andComponents(Pattern 5) targets declaredependencies: []. Confirm by reading those target definitions inPackages/Package.swift; a non-empty list is a violation. -
Fonts register via CoreText against
Bundle.module, never Info.plist orBundle.main(Pattern 6):
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 importumbrella (Pattern 5, AllComponents) is imported only from preview/demo apps and component libraries, never from a production feature path:
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).