PureXML/third_party/rules-swift/exp/implementation-patterns.md

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 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)

// 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 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
// 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:

  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

// 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 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):

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:
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).