PureXML/third_party/rules-swift/exp/implementation-patterns.md
Mihaela Mihaljevic c7ef8abfd9 chore(rules): pin rules-swift corpus, add gates and CI; drop redundant docs/rules mirror
Adopt the rules-swift pin-and-codify strategy in snapshot mode (third_party/rules-swift
@97dc70f + sha256), add codified gates, a conformance audit ledger, and
.github/workflows/rules-swift.yml (corpus-pin integrity required, full gate report mode).
Remove the docs/rules mirror it supersedes (repo-specific files kept under docs/). Style
and leak gates exclude the vendored corpus.
2026-06-30 14:02:40 +02:00

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

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

Acceptance check

The patterns are conforming when their checkable invariants hold:

  • Foundation and core-infrastructure packages 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 package used only off the production path.

  • Observable checklist for the structural shape, each a clear pass/fail: the API layer keeps its four-package 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 packages stay single-purpose with minimal dependencies (Pattern 3); and the component layer is created in order Components, SharedComponents, AppComponents, AllComponents with the Components package never skipped (Pattern 5).