6.7 KiB
Dependency Management
Unidirectional flow, circular-dependency prevention, and platform-specific dependency separation.
Unidirectional Flow
Foundation (SharedModels, AppColors, AppFont)
↓
Design System (AppTheme = AppColors + AppFont)
↓
Infrastructure (ApiClient, ApiServer, Middleware, SharedViews)
↓
Components (Components [core], SharedComponents, AppComponents)
↓
Features (AuthFeature, AppFeature, etc.)
↓
Apps (iosApp, macApp)
Imports and manifest dependencies point downward, from a higher-level consumer to a lower-level provider. NEVER import from a higher layer. Build order and dependent propagation run upward and are not the dependency direction described here.
Circular Dependency Prevention
❌ DON'T:
// AuthFeature → AppFeature
// AppFeature → AuthFeature
// CIRCULAR DEPENDENCY!
✅ DO:
// Extract shared code to a new target
// Both features depend on: SharedAuthModels
Platform-Specific Dependencies
Separate platform-specific products and targets using #if os() conditionals.
Products Separation
// ---------- Base Products (All Platforms) ----------
let baseProducts: [Product] = [
.singleTargetLibrary("ApiShared"),
.singleTargetLibrary("ApiClient"),
.singleTargetLibrary("ApiServer"),
.singleTargetLibrary("SharedModels"),
.singleTargetLibrary("OpenAPICachingMiddleware"),
.executable(name: "apiserverapp", targets: ["ApiServerApp"]),
]
// ---------- Apple-Only Products (iOS + macOS) ----------
#if os(iOS) || os(macOS)
let appleOnlyProducts: [Product] = [
.singleTargetLibrary("AppTheme"),
.singleTargetLibrary("SharedViews"),
.singleTargetLibrary("AuthFeature"),
.singleTargetLibrary("AppFeature"),
.singleTargetLibrary("AppFont"),
.singleTargetLibrary("BetaSettingsFeature"),
.singleTargetLibrary("DemoAppFeature"),
.singleTargetLibrary("SharedComponents"),
.singleTargetLibrary("Components"),
.singleTargetLibrary("BenefitsComponents"),
.singleTargetLibrary("AllComponents"),
]
#else
let appleOnlyProducts: [Product] = []
#endif
// ---------- Combine All Products ----------
let allProducts = baseProducts + appleOnlyProducts + [
.singleTargetLibrary("PlaybookFeature"), // Always exposed for Xcode scheme visibility
]
Targets Separation
let targets: [Target] = {
// ---------- Base Targets (All Platforms) ----------
let sharedModelsTarget = Target.target(
name: "SharedModels",
dependencies: []
)
let apiClientTarget = Target.target(
name: "ApiClient",
dependencies: ["ApiShared", "SharedModels"]
)
let baseTargets = [
sharedModelsTarget,
apiClientTarget,
]
// ---------- Apple-Only Targets (iOS + macOS) ----------
#if os(iOS) || os(macOS)
let appColorsTarget = Target.target(
name: "AppColors",
dependencies: [] // Foundation: zero dependencies
)
let appThemeTarget = Target.target(
name: "AppTheme",
dependencies: [
"AppColors", // Design system combines colors + fonts
"AppFont",
]
)
let sharedViewsTarget = Target.target(
name: "SharedViews",
dependencies: [
"AppColors",
"AppFont",
.product(name: "Inject", package: "Inject"),
]
)
let authFeatureTarget = Target.target(
name: "AuthFeature",
dependencies: ["SharedModels", "SharedViews", "AppColors", "AppFont"]
)
let appleTargets = [
appColorsTarget,
appThemeTarget,
sharedViewsTarget,
authFeatureTarget,
]
#else
let appleTargets: [Target] = []
#endif
// ---------- PlaybookFeature (Always Defined, Conditionally Linked) ----------
let playbookTarget = Target.target(
name: "PlaybookFeature",
dependencies: [
"Components",
"AppComponents",
"SharedModels",
"ApiClient",
.product(name: "Inject", package: "Inject"),
.product(
name: "Playbook",
package: "playbook-ios",
condition: .when(platforms: [.iOS]) // ← Platform-specific dependency
),
.product(
name: "PlaybookUI",
package: "playbook-ios",
condition: .when(platforms: [.iOS])
),
]
)
return baseTargets + appleTargets + [playbookTarget]
}()
Why Separate Platforms?
Benefits:
- ✅ Server targets build on Linux CI without Apple SDKs
- ✅ UI targets only compile on Apple platforms
- ✅ Clear separation between backend and frontend code
- ✅ Prevents accidental dependencies on Apple frameworks in server code
- ✅ Faster CI builds (Linux can skip UI targets)
When to Use:
- UI components, SwiftUI views →
#if os(iOS) || os(macOS) - Shared models, API contracts → No conditional (base products)
- Server code, Vapor endpoints → No conditional (base products)
- Apple-only frameworks (UIKit, AppKit) →
#if os(iOS) || os(macOS)
Use #if os() for platform detection in Package.swift, never #if canImport(UIKit). #if canImport is evaluated lazily and silently breaks the manifest on Linux CI; #if os() is evaluated at manifest-parse time and gives deterministic builds across all platforms. This is the most common SPM landmine on cross-platform packages.
Acceptance check
Conforms when:
- Manifest platform detection uses
#if os(), never#if canImport. This is the named SPM landmine:#if canImport(UIKit)silently breaks the manifest on Linux CI.
MANIFEST=Package.swift
[ -f "$MANIFEST" ] || MANIFEST=Packages/Package.swift
grep -n '#if canImport' "$MANIFEST"
Output MUST be empty; platform splits in the manifest use #if os(iOS) || os(macOS).
- No dependency cycle. Resolution and build succeed, proving the circular-dependency-prevention rule's floor:
swift package resolve && swift build
Both MUST exit 0.
- Observable checklist for the flow rules, each a clear pass/fail: imports and
manifest dependencies go only from apps toward features, components,
infrastructure, design system, and foundation, with no lower layer importing a
higher one; when two targets would depend on each other, the shared code is
extracted to a new lower-level target rather than creating a cycle; Apple-only
products and targets sit behind
#if os(iOS) || os(macOS)while shared models, API contracts, and server code stay unconditional (base); and a platform-specific external package dependency is attached withcondition: .when(platforms:)rather than duplicated. A server target that builds on Linux CI without Apple SDKs is the observable signal that the separation holds.