11 KiB
Package and Repository Structure
Choose the repository shape before adding targets. A Swift package repository is an SPM-consumable package at the repository root. A project repository is a monorepo that hosts one package plus one or more app projects. Do not mix the two shapes casually.
What this covers
This rule covers two allowed shapes:
- Package repository: a reusable package or CLI that another project can add
as a SwiftPM dependency by pointing at the repository URL. It has a root
Package.swift, rootSources/, rootTests/, and noApps/or.xcworkspace. - Project monorepo: an app/product repository that needs Xcode app projects,
workspace settings, signing, entitlements, or multiple app targets. It has
Main.xcworkspace,Packages/Package.swift,Packages/Sources/,Packages/Tests/, andApps/.
Both shapes use one manifest for all library, executable, and test targets in that repository. Add targets when a responsibility becomes separable, reuse appears, isolated compilation matters, or a second front door appears.
Core Rules
Rule 1: Pick exactly one repository shape
Use package repository shape for reusable libraries, reusable CLIs, compilers, parsers, semantic engines, validators, and other code products meant to be consumed by SwiftPM.
Use project monorepo shape for shipping GUI apps or product repositories that need Xcode project settings a plain SwiftPM executable target cannot express.
Do not leave a package repo half-converted: no root Package.swift plus nested
Packages/Package.swift, no orphaned Main.xcworkspace, and no empty Apps/.
Rule 2: Package repository root structure
Package repositories use this top-level structure:
Package.swiftfor all products and targetsSources/for all library and executable targetsTests/for all test targetsdocs/for documentationscripts/for local validation helpers, when needed
Forbidden in package repositories:
Main.xcworkspaceApps/Packages/Package.swiftPackages/Sources/orPackages/Tests/as the primary source tree
Rule 3: Project monorepo root structure
Project monorepos use this top-level structure:
Main.xcworkspacecontaining all projectsPackages/for the single SPM package with all library and CLI targetsApps/for app targets that ship a UIdocs/for documentationscripts/for local validation helpers, when needed
The workspace hosts Packages/Package.swift and any Apps/*/*.xcodeproj. CLI
executable targets stay inside the single Package.swift; Apps/ is for app
targets that need Xcode project settings.
Rule 4: Single Package.swift
Use one active Package.swift for all Swift targets in the repository:
- In a package repo, it is
Package.swiftat the repository root. - In a project monorepo, it is
Packages/Package.swift. - It contains all library targets and products.
- It contains CLI executable targets.
- It contains all test targets.
- It uses
#if os()for platform-specific manifest branches.
Do not split the targets into one manifest per library. A single manifest keeps the dependency graph in one readable place.
Rule 5: Apps as separate projects in project monorepos
App targets are separate Xcode projects in Apps/:
- Each app has its own
.xcodeproj. - Apps import the package as a local SwiftPM dependency.
- This enables signing, entitlements, asset catalogs, and app configurations.
- It supports multiple platforms per app.
The CLI executable target stays inside the single Package.swift. Use Apps/
only for app targets that need Xcode project settings.
Rule 6: Workspace references for project monorepos
Add every project to the workspace: the Packages/ SPM package, each
Apps/*/*.xcodeproj, and the docs/README files.
Rule 7: No storyboards or XIBs
For any UI you add, do not use Interface Builder artifacts:
- NO
.storyboardfiles. - NO
.xibfiles. - ALL UI is created in code (SwiftUI or programmatic UIKit/AppKit).
- Delete any auto-generated storyboards from Xcode templates.
Rule 8: UI code lives in packages
Keep views and view controllers in package targets, not in app targets:
- SwiftUI views in feature targets.
- UIKit/AppKit views in a dedicated UI package.
- App targets contain ONLY entry points (
AppDelegate,SceneDelegate,@main).
Package Repository Layout
PackageRepo/
├── Package.swift # All products and targets
├── Package.resolved # Tracked when dependencies are resolved
├── Sources/
│ ├── PackageCore/
│ ├── PackageCLI/
│ └── ...
├── Tests/
│ ├── PackageCoreTests/
│ └── ...
├── docs/
├── scripts/
└── README.md
Project Monorepo Layout
ProjectRoot/
├── Main.xcworkspace/ # Hosts the package and Apps/ projects
│ └── contents.xcworkspacedata
├── Packages/
│ ├── Package.swift # All package products and targets
│ ├── Package.resolved
│ ├── Sources/
│ │ ├── ProjectCore/
│ │ ├── ProjectCLI/
│ │ └── ...
│ └── Tests/
│ ├── ProjectCoreTests/
│ └── ...
├── Apps/
│ └── ProjectApp/
│ ├── ProjectApp.xcodeproj/
│ └── ProjectApp/
├── docs/
├── scripts/
└── README.md
Package.swift structure (many targets in one package)
Use helper-driven, grouped target declarations rather than one giant inline array.
Platform-specific products
// swift-tools-version: 6.2
import PackageDescription
// ---------- Base Products (All Platforms) ----------
let baseProducts: [Product] = [
.singleTargetLibrary("TileKit"),
.singleTargetLibrary("TileCore"),
.executable(name: "tile-down", targets: ["TileDownCLI"]),
]
// ---------- Apple-Only Products ----------
#if os(iOS) || os(macOS)
let appleOnlyProducts: [Product] = [
.singleTargetLibrary("TileUI"),
]
#else
let appleOnlyProducts: [Product] = []
#endif
let allProducts = baseProducts + appleOnlyProducts
extension Product {
static func singleTargetLibrary(_ name: String) -> Product {
.library(name: name, targets: [name])
}
}
Target organization pattern
let targets: [Target] = {
// ---------- Foundation Layer ----------
let tileCoreTarget = Target.target(
name: "TileCore",
dependencies: []
)
let tileCoreTestsTarget = Target.testTarget(
name: "TileCoreTests",
dependencies: ["TileCore"]
)
let foundationTargets = [tileCoreTarget, tileCoreTestsTarget]
// ---------- Library Layer ----------
let tileKitTarget = Target.target(
name: "TileKit",
dependencies: ["TileCore"]
)
let libraryTargets = [tileKitTarget]
return foundationTargets + libraryTargets
}()
App configuration (only if a GUI app is added)
Removing storyboard references
When you create a new Xcode app project, remove all storyboard configuration.
iOS (UIKit):
- Delete
Main.storyboardandLaunchScreen.storyboard. - Remove the
UIMainStoryboardFileandUILaunchStoryboardNamekeys fromInfo.plist. - Configure the scene manifest with a
UISceneDelegateClassNameand noUISceneStoryboardFilekey. - Provide a launch screen via the
UILaunchScreenplist dictionary instead of a storyboard. - Clear the storyboard build-setting fields.
macOS (AppKit):
- Delete
Main.storyboard/MainMenu.xib. - Remove
NSMainStoryboardFileandNSMainNibFilefromInfo.plist. - Set
NSPrincipalClasstoNSApplication. - Clear the Main Interface build setting.
- Provide a
main.swift(AppKit without a storyboard needs an explicit entry point, no@main):
import Cocoa
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
macOS (SwiftUI):
- Delete any
.storyboard/.xibfiles. - Use a
@mainAppstruct. No storyboard keys are needed.
Build menus in code
For AppKit apps, build the NSMenu programmatically in the app delegate rather than using MainMenu.xib.
Native UI patterns (AppKit / UIKit)
If you add a UIKit/AppKit UI package, use platform typealiases and factory entry points so the app target stays minimal:
#if os(macOS)
import AppKit
public typealias PlatformViewController = NSViewController
public typealias PlatformView = NSView
public typealias PlatformColor = NSColor
#elseif os(iOS)
import UIKit
public typealias PlatformViewController = UIViewController
public typealias PlatformView = UIView
public typealias PlatformColor = UIColor
#endif
public enum NativeUI {
#if os(macOS)
@MainActor
public static func createWindowController() -> NSWindowController {
NativeWindowController()
}
#endif
}
The app target then just wires the factory:
// SceneDelegate (iOS)
window = NativeUI.createMainWindow(for: windowScene) // all UI from the package
When to create a new app target
Create a new app target when:
- A different backend endpoint is needed (local, staging, production).
- A different platform is targeted (iOS, macOS).
- A different app variant ships (lite, pro).
- A different testing mode is needed (offline, mock data).
Do NOT create a new app target when:
- You only need Debug vs Release (use build configurations).
- You only need a different bundle ID (use build settings).
- You only have a feature-flag difference (use runtime flags).
Common mistakes
- Do NOT leave
Packages/Package.swiftin a package repository. The manifest lives at the repository root. - Do NOT leave
Main.xcworkspaceorApps/in a package repository. Those are project monorepo artifacts. - Do NOT put app executable targets for shipping GUI apps in
Package.swiftwhen they need real Xcode project settings. A plain CLI executable target is fine inPackage.swift. - Do NOT create multiple active
Package.swiftfiles, one per library. Use a single manifest. - Do NOT put business logic, view models, services, or models in an app target. They belong in package targets under the active source tree.
- Do NOT keep
.storyboardor.xibfiles anywhere. - Do NOT keep view controllers in the app target. Move them into a package.
Checklist
Before changing repo structure:
- Repository shape is explicitly package repo or project monorepo
- Package repo has root
Package.swift, rootSources/, rootTests/ - Package repo has no
Main.xcworkspace, noApps/, no nestedPackages/Package.swift - Project monorepo has
Main.xcworkspace,Packages/Package.swift,Packages/Sources/,Packages/Tests/ - Project monorepo GUI apps, if any, are separate
.xcodeprojprojects inApps/ - Apps import the package via a local dependency
- Single active SPM package manifest for all targets
- Platform-specific targets use
#if os() - App targets contain entry points only
- Business logic stays in packages, not apps
- No
.storyboardor.xibfiles anywhere - macOS AppKit apps have
main.swift(not@main) - SwiftUI apps use a
@mainAppstruct
Related rules
- package-architecture.md: single-responsibility targets, layers, and the when-to-create decision tree
- package-import-contract.md: what each target may import
- shared-protocols.md: the cross-target protocol-seam target