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.
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.xcworkspacecontaining all projectsPackages/for the single SPM package with all library and CLI targetsApps/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
.storyboardfiles. - NO
.xibfiles. - 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):
- 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 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
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 targets under
Packages/Sources/. - 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:
- 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
.xcodeprojinApps/ - 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
.storyboardor.xibfiles anywhere - macOS AppKit apps have
main.swift(not@main) - SwiftUI apps use a
@mainAppstruct
Related rules
- package-architecture.md: single-responsibility packages, 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 package