Mirror the shared Swift rules canon (rules-swift, commit 4ebb892) into docs/rules-swift/ as a read-only reference alongside the repo's tailored docs/rules/. PROVENANCE.md records the source, commit, and what was included or excluded; the upstream repo's own VCS, CI, and lint tooling is omitted as machinery rather than rule content. Exclude the vendored tree from the style gate (scripts/check-style.sh) and the pre-commit hook: the canon's enforcement docs quote the forbidden attribution phrases to define them, the same reason the repo's own enforcement files are already excluded.
5.2 KiB
5.2 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)
Dependencies only flow downward. NEVER import from a higher layer.
Circular Dependency Prevention
❌ DON'T:
// AuthFeature → AppFeature
// AppFeature → AuthFeature
// CIRCULAR DEPENDENCY!
✅ DO:
// Extract shared code to new package
// 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 packages)
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.