PureXML/third_party/rules-swift/package-structure.md
Mihaela Mihaljevic c7ef8abfd9 chore(rules): pin rules-swift corpus, add gates and CI; drop redundant docs/rules mirror
Adopt the rules-swift pin-and-codify strategy in snapshot mode (third_party/rules-swift
@97dc70f + sha256), add codified gates, a conformance audit ledger, and
.github/workflows/rules-swift.yml (corpus-pin integrity required, full gate report mode).
Remove the docs/rules mirror it supersedes (repo-specific files kept under docs/). Style
and leak gates exclude the vendored corpus.
2026-06-30 14:02:40 +02:00

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, root Sources/, root Tests/, and no Apps/ 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/, and Apps/.

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.swift for all products and targets
  • Sources/ for all library and executable targets
  • Tests/ for all test targets
  • docs/ for documentation
  • scripts/ for local validation helpers, when needed

Forbidden in package repositories:

  • Main.xcworkspace
  • Apps/
  • Packages/Package.swift
  • Packages/Sources/ or Packages/Tests/ as the primary source tree

Rule 3: Project monorepo root structure

Project monorepos use this top-level structure:

  • Main.xcworkspace containing all projects
  • Packages/ for the single SPM package with all library and CLI targets
  • Apps/ for app targets that ship a UI
  • docs/ for documentation
  • scripts/ 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.swift at 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 .storyboard files.
  • NO .xib files.
  • 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 packages.
  • 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):

  1. Delete Main.storyboard and LaunchScreen.storyboard.
  2. Remove the UIMainStoryboardFile and UILaunchStoryboardName keys from Info.plist.
  3. Configure the scene manifest with a UISceneDelegateClassName and no UISceneStoryboardFile key.
  4. Provide a launch screen via the UILaunchScreen plist dictionary instead of a storyboard.
  5. Clear the storyboard build-setting fields.

macOS (AppKit):

  1. Delete Main.storyboard / MainMenu.xib.
  2. Remove NSMainStoryboardFile and NSMainNibFile from Info.plist.
  3. Set NSPrincipalClass to NSApplication.
  4. Clear the Main Interface build setting.
  5. 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):

  1. Delete any .storyboard / .xib files.
  2. Use a @main App struct. 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.swift in a package repository. The manifest lives at the repository root.
  • Do NOT leave Main.xcworkspace or Apps/ in a package repository. Those are project monorepo artifacts.
  • Do NOT put app executable targets for shipping GUI apps in Package.swift when they need real Xcode project settings. A plain CLI executable target is fine in Package.swift.
  • Do NOT create multiple active Package.swift files, 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 .storyboard or .xib files 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, root Sources/, root Tests/
  • Package repo has no Main.xcworkspace, no Apps/, no nested Packages/Package.swift
  • Project monorepo has Main.xcworkspace, Packages/Package.swift, Packages/Sources/, Packages/Tests/
  • Project monorepo GUI apps, if any, are separate .xcodeproj projects in Apps/
  • 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 .storyboard or .xib files anywhere
  • macOS AppKit apps have main.swift (not @main)
  • SwiftUI apps use a @main App struct