Guides expert-level swift project setup implementation: swift and best-practices decision frameworks, production-ready patterns, and concrete templates for swift project setup workflows.
A skill is a written procedure a bot loads when a job calls for it. This one is a single file, SKILL.md, and the whole file is on this page.
Guides expert-level swift project setup implementation: swift and best-practices decision frameworks, production-ready patterns, and concrete templates for swift project setup workflows.
How much does it cost?+
Nothing. It ships with Murage, which is open source. Your bots run on the AI plan you already have.
Can I change it?+
Yes. Open it in Settings → Skills to read, edit or duplicate it. Skill Guard scans it again after every edit.
Give your first job to Murage.
Download the app, connect the AI you already pay for, and tell your Chief of Staff what needs doing. Plan on about ten minutes from install to a working team.
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 @available guards or Combine bridging
Produce a checklist before proceeding:
☐To do:
Platform targets and minimum deployment versions confirmed
☐To do:
Project type (app, library, CLI, server) confirmed
☐To do:
SPM vs. Xcode project vs. hybrid approach decided
☐To do:
Swift version confirmed (minimum Swift 5.9 for macros, 5.10 for ~Copyable, 6.0 for full strict concurrency)
☐To do:
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.
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.
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.
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
// 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 sharedSwiftSettings at 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) from targets (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 .plugin targets for build tool integrations (SwiftLint as a build plugin, Sourcery code generation) rather than running them as pre-build scripts in Xcode
Avoid .unsafeFlags in 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:
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:
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:
DomainModels must 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.
NetworkingKit depends on DomainModels for 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 DomainModels or a shared SharedFeatureKit module
Use @testable import only in test targets. Production targets must only use public and internal APIs
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):
swift
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
// Treat Sendable warnings as errors in new code
.unsafeFlags(["-warnings-as-errors"], .when(configuration: .release))
]
For Swift 6 projects:
swift
swiftSettings: [
.swiftLanguageVersion(.v6)
]
Actor isolation decisions:
Use @MainActor on all View Models (ObservableObject subclasses or @Observable types) that own UI state -- this eliminates the most common data race in iOS apps
Mark networking and persistence types as actor when they maintain mutable state accessed from multiple concurrency domains
Use nonisolated explicitly 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 (using AsyncStream or 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 = complete in 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):
yaml
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 --parallel on swift test to 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 DomainModels and other platform-agnostic modules
Use cancel-in-progress: true to avoid wasting CI minutes on outdated PR pushes
Cache the .build directory using actions/cache keyed on Package.resolved hash 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:
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 withCheckedContinuation bridges 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 .unsafeFlags in 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 legitimate SwiftSetting values instead.
ALWAYS pin exact versions in Package.resolved for application targets by committing the file to version control. For library packages, commit Package.resolved as 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 SwiftUI appears in DomainModels, 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 control deliberately from day one. Default to internal. Use public only on APIs intentionally exposed to other modules. Use private and fileprivate to scope implementation details. Never leave types as implicitly internal without 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 = complete for all new targets in Xcode 15+ projects. Waiting until Swift 6 to discover data race warnings is a migration nightmare. Address each Sendable violation when the code is fresh -- retroactive conformance to Sendable on 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 1 before and after), (d) license compatible with distribution channel. For App Store apps, GPL licenses are incompatible.
ALWAYS create a Makefile or scripts/ directory with standardized build targets. New team members should be able to run make setup, make build, make test, and make lint without 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.swift file 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 to Package.swift, then run pod install followed by swift 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 clang module 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 SwiftSyntax and import SwiftSyntaxMacros -- add swift-syntax as a package dependency (version 509.0.0 for Swift 5.9, 510.0.0 for Swift 5.10, 600.0.0 for Swift 6.0)
Macro implementation targets must have compilerPluginTarget or be declared as .macro target 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 dedicated MacroTests test target
Macro compilation is slow on first build -- swift build times 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.sh is 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 (.xctestplan files) 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 -- Foundation is available on Linux but many of its APIs behave differently (e.g., DateFormatter locale handling, JSONDecoder key decoding behavior on Linux vs. Darwin)
Codable works cross-platform but NSCoding does not -- avoid NSCoding in shared modules
UUID, Date, and Decimal are all available cross-platform via Foundation
FileManager works on Linux but path separators differ -- use URL-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.swift files that are difficult to reason about
Excessive public API 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.swift files to point to internal URLs
Use --disable-automatic-resolution flag with swift package resolve to 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 @MainActor on delegate protocol methods), global mutable state (convert to actors or nonisolated(unsafe) with explicit documentation), and Objective-C interop types not marked @Sendable
Use SWIFT_STRICT_CONCURRENCY = targeted as 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?"
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.