PureYAML/docs/rules-swift/package-structure.md
Mihaela M 5f3892f2c1 chore(rules): vendor the rules-swift canon into docs/rules-swift
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.
2026-06-26 05:36:12 +02:00

9.9 KiB

Package and Repository Structure

How the repository is laid out. Tiledown is a monorepo from day one: a workspace, a single Package.swift under Packages/, many single-responsibility targets in that one package, and Apps/ for app targets. Sources live in Packages/Sources/, tests in Packages/Tests/. The TileKit library and the tile-down executable are already two targets in that package, so the layout rules below apply now.

What this covers

Tiledown ships as one SPM package with many targets, not one package per library. There is a single Package.swift under Packages/; the TileKit library and the tile-down CLI are two targets in it today, and new targets join the same manifest as responsibilities separate out. The workspace and Apps/ directory are part of the structure from the start; Apps/ holds app targets (such as the planned native macOS/iOS editor) while the CLI executable target stays in Package.swift.

Reach for additional targets in the single package when one of these is true:

  • A part of TileKit becomes a clearly separable responsibility (a parser, a transport, a renderer) used in more than one place.
  • A second app target appears (for example the GUI editor alongside the CLI).
  • You want isolated compilation and parallel builds across several focused targets.

Core rules

Rule 1: Root structure

Organize the repository with these top-level directories:

  • 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 (such as the planned native editor)
  • docs/ for documentation

The workspace hosts the Packages/ SPM package and any Apps/*/*.xcodeproj. The CLI executable target stays inside the single Package.swift; Apps/ is for app targets that need Xcode project settings a plain SPM executable target cannot express.

Rule 2: Single Package.swift

Use ONE Package.swift for all targets:

  • It contains ALL library targets and products.
  • It contains the CLI executable target.
  • It contains ALL test targets.
  • It uses #if os() for platform-specific targets.
  • App projects reference the package via a local path dependency.

Do not split the targets into one Package.swift per library. A single manifest keeps the dependency graph in one readable place.

Rule 3: Apps as separate projects

App targets (such as the planned native macOS/iOS editor) are separate Xcode projects in Apps/:

  • Each app has its own .xcodeproj.
  • Apps import the package as a local SPM dependency.
  • This enables different app configurations (Debug, Release, alternate backends).
  • It supports multiple platforms per app.

The CLI executable target stays inside the single Package.swift. Use Apps/ for app targets that need Xcode project settings a plain SPM executable target cannot express.

Rule 4: Workspace references

Add every project to the workspace: the Packages/ SPM package, each Apps/*/*.xcodeproj, and the docs/README files.

Rule 5: 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 6: UI code lives in packages

Keep views and view controllers in packages, 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).

Directory structure

TileDownRoot/
├── Main.xcworkspace/              # Hosts the package and any Apps/ projects
│   └── contents.xcworkspacedata
├── Packages/                      # Single SPM package
│   ├── Package.swift              # ALL targets defined here
│   ├── Package.resolved
│   ├── Sources/
│   │   ├── TileKit/               # the engine library
│   │   ├── TileDownCLI/           # the tile-down CLI target
│   │   └── ...                    # additional focused libraries
│   └── Tests/
│       ├── TileKitTests/
│       └── ...
├── Apps/                          # App targets (planned native editor)
│   └── TileDownApp/
│       ├── TileDownApp.xcodeproj/
│       └── TileDownApp/
├── docs/
└── 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.0
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 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 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 targets under Packages/Sources/.
  • 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:

  • Single SPM package manifest for all targets
  • All library and CLI code under Packages/Sources/
  • All test code under Packages/Tests/
  • Each GUI app, if any, is a separate .xcodeproj in Apps/
  • Apps import the package via a local dependency
  • 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