Swift Project Setup
When to Use
Use this skill when:
- User is initializing a new Swift package, iOS app, macOS app, or server-side Swift service and needs a production-ready project structure
- User wants to configure Swift Package Manager (SPM) with multiple targets, products, and dependencies
- User is migrating from CocoaPods or Carthage to SPM and needs guidance on the Package.swift manifest
- User needs to set up a multi-module Swift project with clean separation between feature modules, domain layers, and infrastructure layers
- User wants to establish linting (SwiftLint), formatting (SwiftFormat), code generation (Sourcery), and documentation (DocC) tooling for a Swift project
- User needs to configure Swift concurrency settings (strict concurrency checking, actor isolation) for a new codebase
- User is setting up a Swift project with mixed targets -- for example, a library with both iOS and macOS deployment targets, plus a command-line test harness
Do NOT use this skill when:
- User is asking about Objective-C project setup -- this skill is Swift-only; defer to language-migration guidance if bridging is involved
- User needs Xcode workspace or scheme configuration for a complex multi-app monorepo -- that is a separate CI/CD and workspace management concern
- User is asking about SwiftUI architecture patterns (MVVM, TCA, MV) -- check the swift-architecture-patterns skill
- User needs help writing Swift tests or test plans -- check the swift-testing skill
- User is asking about Swift performance profiling with Instruments -- check the swift-performance-optimization skill
- User needs help configuring App Store Connect, provisioning profiles, or code signing -- check the ios-code-signing skill
- User wants general Swift language syntax help -- this skill is specifically about project structure and setup
Process
1. Gather Project Context and Target Platforms
Before recommending any structure, collect the following information:
- Target platform(s): iOS (minimum deployment target -- typically 16.0+ for Swift concurrency without continuations), macOS (13.0+), watchOS, tvOS, visionOS, Linux (for server-side Swift or CLI tools)
- Project type: Application (executable), library (static or dynamic), framework, Swift macro, command-line tool, or server-side service
- Distribution channel: App Store, SPM package registry, internal enterprise, or self-hosted
- Team size and tooling maturity: Solo developer vs. team of 5+ engineers determines how much linting automation and module enforcement to impose from day one
- Existing codebase: New greenfield project vs. migration from CocoaPods/Carthage vs. Xcode project without SPM
- Concurrency model: Determine whether the project will use async/await throughout, or must support iOS 14/15 compatibility requiring
@availableguards or Combine bridging
Produce a checklist before proceeding:
- Platform targets and minimum deployment versions confirmed
- Project type (app, library, CLI, server) confirmed
- SPM vs. Xcode project vs. hybrid approach decided
- Swift version confirmed (minimum Swift 5.9 for macros, 5.10 for
~Copyable, 6.0 for full strict concurrency) - Monorepo vs. single-package structure decided
2. Choose the Project Layout Strategy
Select one of three layouts based on project type and scale:
Layout A -- Single-Package Library or CLI Tool: Use when building a reusable library or CLI with no UI. The entire project lives in one Package.swift with clearly named targets.
MyPackage/
├── Package.swift
├── Sources/
│ ├── MyPackage/ # Main library target
│ └── MyPackageCLI/ # Optional CLI executable target
├── Tests/
│ └── MyPackageTests/
└── Plugins/ # Optional build tool or command plugins
Layout B -- iOS/macOS App with Feature Modules: Use when building an application that will grow beyond 50K lines of code or when multiple developers work on distinct features simultaneously. Use a local package to house feature modules, keeping the Xcode project thin.
MyApp/
├── MyApp.xcodeproj (or .xcworkspace)
├── MyApp/ # App target sources (entry point, AppDelegate/App)
├── MyAppPackage/ # Local SPM package containing all modules
│ ├── Package.swift
│ └── Sources/
│ ├── FeatureHome/
│ ├── FeatureProfile/
│ ├── DomainModels/
│ ├── NetworkingKit/
│ └── DesignSystem/
└── Tests/
Layout C -- Multi-Package Monorepo: Use when multiple Swift packages share code and are developed together (e.g., a shared domain model used by an iOS app and a server-side Swift backend). Requires a workspace-level Package.swift or local path dependencies.
monorepo/
├── packages/
│ ├── SharedDomain/
│ ├── iOSApp/
│ └── ServerApp/
├── Makefile
└── scripts/
Decision rule: If the project is a pure library with no app target, use Layout A. If it is an app with more than 3 logical feature areas or a team of 3+ developers, use Layout B from day one -- retrofitting modularization later is expensive. Use Layout C only when two separate deployables (app + server) genuinely share domain types.
3. Write the Package.swift Manifest
A well-formed Package.swift is the foundation of a Swift project. Apply these patterns:
Swift Tools Version:
Always set swift-tools-version to the minimum version your team's Xcode or Swift toolchain supports. As of 2024, 5.9 is the safe minimum for most projects; use 5.10 for ~Copyable support; use 6.0 only if the entire team uses Xcode 16+ and the project targets Swift 6.
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [
.iOS(.v16),
.macOS(.v13)
],
products: [
.library(name: "DomainModels", targets: ["DomainModels"]),
.library(name: "NetworkingKit", targets: ["NetworkingKit"]),
.executable(name: "MyAppCLI", targets: ["MyAppCLI"])
],
dependencies: [
// Pin to exact minor versions in apps, use version ranges in libraries
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),
.package(url: "https://github.com/apple/swift-log", from: "1.5.0"),
],
targets: [
.target(
name: "DomainModels",
dependencies: [],
path: "Sources/DomainModels",
swiftSettings: sharedSwiftSettings
),
.target(
name: "NetworkingKit",
dependencies: [
"DomainModels",
.product(name: "Logging", package: "swift-log")
],
path: "Sources/NetworkingKit",
swiftSettings: sharedSwiftSettings
),
.testTarget(
name: "NetworkingKitTests",
dependencies: ["NetworkingKit"],
path: "Tests/NetworkingKitTests"
)
]
)
// Define shared Swift settings as a constant to avoid repetition
let sharedSwiftSettings: [SwiftSetting] = [
.enableUpcomingFeature("BareSlashRegexLiterals"),
.enableUpcomingFeature("ConciseMagicFile"),
.enableExperimentalFeature("StrictConcurrency"),
// Remove the above and use the below when targeting Swift 6:
// .swiftLanguageVersion(.v6)
]
Key rules for the manifest:
- Use
let sharedSwiftSettingsat the top of the file to apply consistent compiler flags across all targets -- do not copy-paste settings per target - Separate
products(public-facing API surface) fromtargets(build graph nodes) -- a target is an implementation detail; a product is a contract - Use
.package(path: "../SharedDomain")for local package dependencies within a monorepo -- never use file URLs pointing outside the repository root - Define
.plugintargets for build tool integrations (SwiftLint as a build plugin, Sourcery code generation) rather than running them as pre-build scripts in Xcode - Avoid
.unsafeFlagsin any target destined for the App Store or package registry -- it prevents downstream consumers from using the package
4. Configure Code Quality Tooling
Set up the following tools in this order: formatting first, linting second, code generation third.
SwiftFormat:
Create .swiftformat in the project root. Key settings for a consistent codebase:
--swiftversion 5.9
--indent 4
--maxwidth 120
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--commas always
--semicolons never
--importgrouping testable-bottom
--patternlet inline
--redundanttype inferred
--unusedarguments closure-only
--disable trailingCommas # only if team prefers no trailing commas
Run SwiftFormat as a pre-commit hook via .git/hooks/pre-commit or through a Makefile target:
make format # runs: swiftformat . --config .swiftformat
SwiftLint:
Create .swiftlint.yml in the project root. Do not enable all rules by default -- start with opt_in_rules for the highest-value additions:
included:
- Sources
- Tests
excluded:
- .build
- DerivedData
disabled_rules:
- trailing_whitespace # handled by SwiftFormat
opt_in_rules:
- array_init
- closure_end_indentation
- conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- discouraged_optional_boolean
- empty_collection_literal
- empty_string
- explicit_init
- fallthrough
- file_name_no_space
- first_where
- force_unwrapping
- ibinspectable_in_extension
- implicit_return
- last_where
- literal_expression_end_indentation
- lower_acl_than_parent
- modifier_order
- multiline_function_chains
- operator_usage_whitespace
- prefer_self_in_static_references
- redundant_nil_coalescing
- sorted_imports
- strict_fileprivate
- toggle_bool
- unneeded_parentheses_in_closure_argument
- vertical_parameter_alignment_on_call
line_length:
warning: 120
error: 160
ignores_comments: true
ignores_urls: true
type_body_length:
warning: 250
error: 400
file_length:
warning: 400
error: 600
function_body_length:
warning: 40
error: 80
cyclomatic_complexity:
warning: 10
error: 20
force_cast: error
force_try:
severity: error
Integrate SwiftLint as an SPM plugin target for library projects. For Xcode app projects, add a Run Script phase that invokes swiftlint lint --strict and fails the build on errors (not warnings) during CI.
Sourcery (if code generation is needed):
Create a sourcery.yml configuration file. Use Sourcery selectively -- AutoMockable protocol stubs for testing and Encodable/Decodable boilerplate are the highest-value use cases:
sources:
- Sources/DomainModels
templates:
- Templates/AutoMockable.stencil
output:
- Tests/Mocks
args:
imports:
- Foundation
5. Set Up the Module Dependency Graph
Establish a strict dependency rule before writing any code -- once violated, it becomes expensive to fix.
Layered Architecture for Apps:
App Target (entry point, no business logic)
└── FeatureModules (UI + feature-specific state)
└── DomainModels (pure Swift types, no UIKit/SwiftUI import)
└── CoreUtilities (extensions, logging, error types)
Rules for the dependency graph:
DomainModelsmust have zero external dependencies and zero Apple framework imports except Foundation. This keeps it testable without any simulator and compilable on Linux for server-side use.NetworkingKitdepends onDomainModelsfor request/response types but never on any feature module -- this prevents circular dependencies- Feature modules may depend on sibling feature modules only if the dependency is downward in the feature hierarchy -- never upward or lateral. If two feature modules need to share something, extract it to
DomainModelsor a sharedSharedFeatureKitmodule - Use
@testable importonly in test targets. Production targets must only usepublicandinternalAPIs
Enforce the dependency graph via SPM -- since SPM only builds what is declared, undeclared dependencies will not compile. This is the primary enforcement mechanism.
6. Configure Swift Concurrency and Type Safety Settings
Apply per-target Swift settings to incrementally adopt Swift 6 strict concurrency:
For new Swift 5.9/5.10 projects (pre-Swift 6):
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
// Treat Sendable warnings as errors in new code
.unsafeFlags(["-warnings-as-errors"], .when(configuration: .release))
]
For Swift 6 projects:
swiftSettings: [
.swiftLanguageVersion(.v6)
]
Actor isolation decisions:
- Use
@MainActoron all View Models (ObservableObject subclasses or@Observabletypes) that own UI state -- this eliminates the most common data race in iOS apps - Mark networking and persistence types as
actorwhen they maintain mutable state accessed from multiple concurrency domains - Use
nonisolatedexplicitly on computed properties and methods of actors that do not access actor-isolated state -- this avoids unnecessary hop to the actor's executor - Prefer
AsyncSequence(usingAsyncStreamor custom implementations) over Combine publishers for new code in Swift 5.9+ -- Combine still has a place in bridging to existing UIKit code - Set
SWIFT_STRICT_CONCURRENCY = completein Xcode build settings for new targets from day one; do not wait until Swift 6
7. Set Up CI Configuration
A production Swift project must build and test in CI from day one. Provide a minimal but complete CI setup:
GitHub Actions (.github/workflows/swift.yml):
name: Swift CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-test:
runs-on: macos-14 # Xcode 15.x
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Resolve dependencies
run: swift package resolve
- name: Build
run: swift build -c release
- name: Test
run: swift test --parallel --enable-code-coverage
- name: Lint
run: swiftlint lint --strict --reporter github-actions-logging
- name: Format check
run: swiftformat --lint . --config .swiftformat
build-linux:
runs-on: ubuntu-latest
container: swift:5.9
steps:
- uses: actions/checkout@v4
- name: Build on Linux
run: swift build
- name: Test on Linux
run: swift test --parallel
Key CI decisions:
- Run
--parallelonswift testto exercise concurrency code and reduce build time -- this typically cuts test time by 40-60% on multi-core CI machines - The Linux build job catches accidental Apple-framework imports in
DomainModelsand other platform-agnostic modules - Use
cancel-in-progress: trueto avoid wasting CI minutes on outdated PR pushes - Cache the
.builddirectory usingactions/cachekeyed onPackage.resolvedhash to avoid re-resolving dependencies on every run
8. Document Architecture Decisions
Create an ADR/ directory at the project root. Write an ADR for each significant setup decision using the MADR (Markdown Architecture Decision Records) format:
ADR/
├── 0001-use-spm-over-cocoapods.md
├── 0002-module-dependency-direction.md
├── 0003-swift-concurrency-migration-plan.md
└── 0004-swiftlint-rule-configuration.md
Minimum ADR content:
- Status: Accepted / Superseded by / Deprecated
- Context: What forced this decision
- Decision: What was decided
- Consequences: What this makes easier, what it makes harder, what technical debt is incurred
Output Format
When responding to a Swift project setup request, structure the output as follows:
## Swift Project Setup Plan
### Project Profile
| Dimension | Value |
|--------------------|--------------------------------------------|
| Project Type | [Library / iOS App / macOS App / CLI / Server] |
| Swift Version | [5.9 / 5.10 / 6.0] |
| Minimum Deployment | [iOS 16.0, macOS 13.0, etc.] |
| Module Count | [Single / 2-5 modules / 5+ modules] |
| Layout Strategy | [A: Single Package / B: App + Local Package / C: Monorepo] |
| Concurrency Mode | [Strict / Warnings-only / Legacy Async] |
---
### Package.swift
[Complete, runnable Package.swift with all targets and settings]
---
### Directory Structure
[ASCII tree of the complete project layout]
---
### Tooling Configuration
[.swiftformat config, .swiftlint.yml config, any Makefile targets]
---
### Dependency Graph
[ASCII diagram of module dependency directions]
---
### CI Configuration
[Complete GitHub Actions or Xcode Cloud workflow YAML]
---
### Key Decisions and Rationale
| Decision | Chosen Approach | Rationale |
|---------------------------------|--------------------------|----------------------------------------|
| Package manager | SPM | [reason] |
| Module separation | [approach] | [reason] |
| Concurrency strictness | [level] | [reason] |
| Minimum deployment target | [version] | [reason] |
---
### Setup Checklist
- [ ] Package.swift created and resolves without errors
- [ ] All targets compile on release configuration
- [ ] Tests run with `swift test --parallel`
- [ ] SwiftLint passes with zero errors
- [ ] SwiftFormat passes `--lint` check
- [ ] CI pipeline passes on first commit
- [ ] ADR directory initialized with at least one decision record
- [ ] Linux build (if applicable) passes
Rules
NEVER set a minimum deployment target lower than necessary. Every version below iOS 16.0 or macOS 13.0 forces
withCheckedContinuationbridges for async/await and prevents use of@Observable(iOS 17+). Explicitly ask the user what their minimum supported version is -- do not default to iOS 14 or lower without a strong business reason.NEVER use
.unsafeFlagsin any library target that will be published to the SPM package registry or consumed by external packages. Swift will refuse to build packages that depend on a target with.unsafeFlags. Use.enableUpcomingFeature,.enableExperimentalFeature, and legitimateSwiftSettingvalues instead.ALWAYS pin exact versions in
Package.resolvedfor application targets by committing the file to version control. For library packages, commitPackage.resolvedas well but expect to update it frequently. The resolved file prevents "it worked on my machine" dependency drift.NEVER put UI code (SwiftUI, UIKit, AppKit imports) in a module that is supposed to be platform-agnostic. The moment
import SwiftUIappears inDomainModels, the module becomes untestable without a simulator and uncompilable on Linux. Use#if canImport(SwiftUI)guards only when a single module must serve both contexts with explicit justification.ALWAYS use
access controldeliberately from day one. Default tointernal. Usepubliconly on APIs intentionally exposed to other modules. Useprivateandfileprivateto scope implementation details. Never leave types as implicitlyinternalwithout thinking -- the discipline prevents accidental API surface expansion.NEVER create circular dependencies between SPM targets. SPM will fail to build with a clear error, but fixing circular dependencies in an established codebase is extremely costly. If two targets need to share a type, extract it to a third "shared types" target that both depend on.
ALWAYS enable
SWIFT_STRICT_CONCURRENCY = completefor all new targets in Xcode 15+ projects. Waiting until Swift 6 to discover data race warnings is a migration nightmare. Address eachSendableviolation when the code is fresh -- retroactive conformance toSendableon hundreds of types is a multi-day task.NEVER add a third-party dependency without evaluating it against these four criteria: (a) active maintenance within the last 6 months, (b) fewer than 10 unresolved critical issues, (c) compile-time overhead acceptable on a clean build (benchmark with
swift build --jobs 1before and after), (d) license compatible with distribution channel. For App Store apps, GPL licenses are incompatible.ALWAYS create a
Makefileorscripts/directory with standardized build targets. New team members should be able to runmake setup,make build,make test, andmake lintwithout reading any documentation. This is especially important for onboarding and CI consistency.NEVER rely on Xcode's "automatically manage signing" for library packages. Library packages have no code signing requirement -- only executable targets do. Mixing library and app targets in a single Xcode project without understanding this distinction causes spurious signing errors in CI. Keep library packages in pure SPM projects with no Xcode project file unless there is a concrete need for one.
Edge Cases
Migrating from CocoaPods to SPM
CocoaPods and SPM can coexist during migration, but they conflict when both manage the same dependency. Use this approach:
- Identify all Pods that have official SPM support (check the repository for a
Package.swiftfile at the root) - Migrate one pod at a time, starting with leaf dependencies (those with no other pod dependencies)
- Remove the pod from
Podfile, add the SPM equivalent toPackage.swift, then runpod installfollowed byswift package resolve - Test the build before moving to the next pod
- For pods without SPM support, evaluate whether to fork and add SPM support, write a wrapper package, or continue using CocoaPods for that specific dependency until an alternative exists
- Some pods use Objective-C and require bridging headers -- these cannot be migrated to SPM modules cleanly without wrapping the Objective-C in a
clangmodule map inside a.xcframework
Swift Macros (Swift 5.9+)
Macro targets require a special setup that many developers overlook:
- Macro implementation targets must use
import SwiftSyntaxandimport SwiftSyntaxMacros-- addswift-syntaxas a package dependency (version509.0.0for Swift 5.9,510.0.0for Swift 5.10,600.0.0for Swift 6.0) - Macro implementation targets must have
compilerPluginTargetor be declared as.macrotarget type - Macros must be in a separate target from their clients -- you cannot define and use a macro in the same target
- Macro testing requires
SwiftSyntaxMacrosTestSupport-- add a dedicatedMacroTeststest target - Macro compilation is slow on first build --
swift buildtimes increase by 30-120 seconds depending on the macro complexity. Add#warning("Macros slow first build -- this is expected")in the macro target for new team members
Xcode Cloud vs. GitHub Actions
When a project uses Xcode Cloud (Apple's CI):
- The
ci_scripts/directory must be at the Xcode project root (not the SPM package root if they differ) ci_pre_xcodebuild.shis the right place for SwiftLint and SwiftFormat checks -- not a build phase, which would re-run on every incremental build- Xcode Cloud does not support custom Docker containers -- all tools (SwiftLint, SwiftFormat) must be installed via Homebrew in
ci_post_clone.sh - Test plans (
.xctestplanfiles) must be committed to version control for Xcode Cloud to pick them up; ad-hoc scheme test configurations are not picked up automatically
Sharing Code Between iOS App and Server-Side Swift
When a DomainModels target must compile on both Darwin and Linux:
- Audit every import --
Foundationis available on Linux but many of its APIs behave differently (e.g.,DateFormatterlocale handling,JSONDecoderkey decoding behavior on Linux vs. Darwin) Codableworks cross-platform butNSCodingdoes not -- avoidNSCodingin shared modulesUUID,Date, andDecimalare all available cross-platform via FoundationFileManagerworks on Linux but path separators differ -- useURL-based APIs exclusively- Use the Linux CI job (see Process Step 7) as the enforcing mechanism -- it will fail to compile if you accidentally import an Apple-only framework
Large Teams (10+ Engineers) and Module Explosion
Projects with 10+ engineers often over-modularize, creating 30-50 modules where 10 would suffice. This causes:
- Incremental build times that are worse than a single-module project because SPM module rebuild tracking overhead exceeds the compilation savings
- Complex
Package.swiftfiles that are difficult to reason about - Excessive
publicAPI surface scattered across many modules
Counter this with the "pizza rule": each module should be maintainable by a team that can be fed with two pizzas (5-8 engineers). If a module has one primary owner and fewer than 3000 lines of code, it may be too granular -- consider merging it with its closest sibling.
Offline and Air-Gapped Development Environments
In environments with no internet access (government, banking):
- Mirror all SPM dependencies to an internal Git server and update all
Package.swiftfiles to point to internal URLs - Use
--disable-automatic-resolutionflag withswift package resolveto prevent unexpected network calls during builds - Consider using
.package(path:)with vendored source copies instead of URL-based dependencies for the most sensitive environments - Never use SPM's
revision:pinning in production -- it bypasses semantic versioning guarantees and makes auditing dependency versions difficult
Transitioning from Swift 5.x to Swift 6 Strict Concurrency
This is the single most disruptive change in Swift's history for existing codebases:
- Start by adding
.enableExperimentalFeature("StrictConcurrency")per target in Swift 5.9 and fixing all warnings before moving to Swift 6 - The biggest sources of warnings are: delegate patterns (use
@MainActoron delegate protocol methods), global mutable state (convert to actors ornonisolated(unsafe)with explicit documentation), and Objective-C interop types not marked@Sendable - Use
SWIFT_STRICT_CONCURRENCY = targetedas an intermediate setting that catches the most common issues without requiring full actor isolation of the entire module nonisolated(unsafe)is the escape hatch for genuinely unsafe but pragmatically necessary code -- mark every usage with// CONCURRENCY-UNSAFE: [reason]comment for future reference- Plan 1-3 months for a 100K LOC codebase migration to Swift 6 strict concurrency -- this estimate assumes dedicated engineering time, not spare capacity
Example
Input: "I'm starting a new iOS app called 'Meridian' -- a journaling app. The team is 4 engineers. We'll target iOS 16+. We want a modular structure from day one, with separate modules for the journal entry feature, local persistence, and design system. We're using SPM only, no CocoaPods. What's the full setup?"
Output:
Swift Project Setup Plan: Meridian
Project Profile
| Dimension | Value |
|---|---|
| Project Type | iOS Application with Local Package modules |
| Swift Version | 5.9 |
| Minimum Deployment | iOS 16.0 |
| Module Count | 5 modules (see dependency graph below) |
| Layout Strategy | B: iOS App Target + Local SPM Package |
| Concurrency Mode | StrictConcurrency experimental (Swift 5.9) |
Directory Structure
Meridian/
├── Meridian.xcodeproj
├── App/ # Xcode app target -- entry point only
│ ├── MeridianApp.swift
│ ├── ContentView.swift
│ └── Assets.xcassets
├── MeridianPackage/ # Local SPM package containing all modules
│ ├── Package.swift
│ └── Sources/
│ ├── JournalFeature/ # SwiftUI views + ViewModels for journal
│ ├── PersistenceKit/ # SwiftData/CoreData stack, no UI
│ ├── DomainModels/ # JournalEntry, Tag, Mood -- pure Swift
│ ├── DesignSystem/ # Colors, Typography, Reusable components
│ └── CoreUtilities/ # Logging, Error types, Extensions
├── Tests/
│ ├── JournalFeatureTests/
│ ├── PersistenceKitTests/
│ └── DomainModelsTests/
├── ADR/
│ ├── 0001-spm-only-no-cocoapods.md
│ ├── 0002-local-package-modularization.md
│ └── 0003-swiftdata-for-persistence.md
├── .swiftlint.yml
├── .swiftformat
├── Makefile
└── .github/
└── workflows/
└── swift.yml
Package.swift (MeridianPackage/Package.swift)
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MeridianPackage",
platforms: [
.iOS(.v16)
],
products: [
.library(name: "JournalFeature", targets: ["JournalFeature"]),
.library(name: "PersistenceKit", targets: ["PersistenceKit"]),
.library(name: "DomainModels", targets: ["DomainModels"]),
.library(name: "DesignSystem", targets: ["DesignSystem"]),
.library(name: "CoreUtilities", targets: ["CoreUtilities"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-log", from: "1.5.3"),
],
targets: [
// MARK: - Feature Modules
.target(
name: "JournalFeature",
dependencies: [
"DomainModels",
"PersistenceKit",
"DesignSystem",
],
path: "Sources/JournalFeature",
swiftSettings: sharedSettings
),
// MARK: - Infrastructure Modules
.target(
name: "PersistenceKit",
dependencies: [
"DomainModels",
"CoreUtilities",
],
path: "Sources/PersistenceKit",
swiftSettings: sharedSettings
),
// MARK: - Core / Domain Modules
.target(
name: "DomainModels",
dependencies: [], // zero external dependencies -- intentional
path: "Sources/DomainModels",
swiftSettings: sharedSettings
),
.target(
name: "DesignSystem",
dependencies: ["CoreUtilities"],
path: "Sources/DesignSystem",
swiftSettings: sharedSettings
),
.target(
name: "CoreUtilities",
dependencies: [
.product(name: "Logging", package: "swift-log")
],
path: "Sources/CoreUtilities",
swiftSettings: sharedSettings
),
// MARK: - Test Targets
.testTarget(
name: "JournalFeatureTests",
dependencies: ["JournalFeature", "DomainModels"],
path: "Tests/JournalFeatureTests"
),
.testTarget(
name: "PersistenceKitTests",
dependencies: ["PersistenceKit", "DomainModels"],
path: "Tests/PersistenceKitTests"
),
.testTarget(
name: "DomainModelsTests",
dependencies: ["DomainModels"],
path: "Tests/DomainModelsTests"
)
]
)
// Shared Swift settings applied to all production targets
let sharedSettings: [SwiftSetting] = [
.enableUpcomingFeature("BareSlashRegexLiterals"),
.enableUpcomingFeature("ConciseMagicFile"),
.enableExperimentalFeature("StrictConcurrency"),
]
Module Dependency Graph
App (Xcode target)
└── JournalFeature
├── DomainModels (zero deps -- pure Swift structs/enums)
├── PersistenceKit
│ ├── DomainModels
│ └── CoreUtilities
│ └── swift-log (external)
└── DesignSystem
└── CoreUtilities
└── swift-log (external)
Key constraint: DomainModels has zero dependencies. JournalFeature is the only target allowed to import SwiftUI -- DesignSystem imports SwiftUI for reusable components, which is intentional and documented. PersistenceKit imports SwiftData but not SwiftUI.
.swiftformat Configuration
--swiftversion 5.9
--indent 4
--maxwidth 120
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--commas always
--semicolons never
--importgrouping testable-bottom
--patternlet inline
--redundanttype inferred
--unusedarguments closure-only
--header \n Meridian\n Copyright © 2024 Meridian Team. All rights reserved.\n
.swiftlint.yml Configuration
included:
- Sources
- Tests
excluded:
- .build
disabled_rules:
- trailing_whitespace
opt_in_rules:
- array_init
- contains_over_filter_is_empty
- discouraged_optional_boolean
- empty_string
- first_where
- force_unwrapping
- implicit_return
- last_where
- modifier_order
- sorted_imports
- toggle_bool
line_length:
warning: 120
error: 160
ignores_comments: true
ignores_urls: true
force_cast: error
force_try:
severity: error
Makefile
.PHONY: setup build test lint format clean
setup:
brew install swiftlint swiftformat
swift package resolve
build:
swift build -c release
test:
swift test --parallel --enable-code-coverage
lint:
swiftlint lint --strict
format:
swiftformat . --config .swiftformat
format-check:
swiftformat --lint . --config .swiftformat
clean:
swift package clean
rm -rf .build
CI Configuration (.github/workflows/swift.yml)
name: Meridian CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-test:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Select Xcode 15.4
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Cache SPM dependencies
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- name: Resolve dependencies
run: swift package resolve
- name: Build (release)
run: swift build -c release
- name: Run tests
run: swift test --parallel --enable-code-coverage
- name: Check formatting
run: swiftformat --lint . --config .swiftformat
- name: Run SwiftLint
run: swiftlint lint --strict --reporter github-actions-logging
Key Decisions and Rationale
| Decision | Chosen Approach | Rationale |
|---|---|---|
| Package manager | SPM only | No legacy Objective-C pods; SPM has first-class Xcode 15 integration |
| Modularization | 5-module local package inside Xcode project | 4-engineer team benefits from compile-time module boundaries; too few to justify monorepo |
| Persistence | SwiftData (iOS 16+) | Targets iOS 16+ so SwiftData is available; avoids CoreData boilerplate |
| Swift version | 5.9 with StrictConcurrency experimental | Team not yet on Xcode 16; builds foundation for Swift 6 migration |
| Design system as module | Yes, separate DesignSystem target | Enables consistent UI tokens; enforces no business logic in design layer |
| Logging | swift-log (structured logging) | Abstracts log backend; can swap to OSLog or custom handler without source changes |
Setup Checklist
- Run
make setupto install SwiftLint and SwiftFormat and resolve dependencies - Run
make build-- confirm zero errors and zero warnings - Run
make test --parallel-- confirm all test targets build and pass - Run
make lint-- confirm zero SwiftLint errors - Run
make format-check-- confirm SwiftFormat reports no changes needed - Push a commit and verify GitHub Actions CI passes all jobs
- Verify
DomainModelstarget has no Apple framework imports beyond Foundation - Create
ADR/0001-spm-only-no-cocoapods.mddocumenting the decision to use SPM exclusively - Add
Package.resolvedto version control (git add MeridianPackage/Package.resolved)