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.
276 lines
9.9 KiB
Markdown
276 lines
9.9 KiB
Markdown
# 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
|
|
// 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
|
|
|
|
```swift
|
|
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`):
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
#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
|
|
```
|
|
|
|
```swift
|
|
public enum NativeUI {
|
|
#if os(macOS)
|
|
@MainActor
|
|
public static func createWindowController() -> NSWindowController {
|
|
NativeWindowController()
|
|
}
|
|
#endif
|
|
}
|
|
```
|
|
|
|
The app target then just wires the factory:
|
|
|
|
```swift
|
|
// 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
|
|
|
|
## Related rules
|
|
|
|
- [package-architecture.md](package-architecture.md): single-responsibility packages, layers, and the when-to-create decision tree
|
|
- [package-import-contract.md](package-import-contract.md): what each target may import
|
|
- [shared-protocols.md](shared-protocols.md): the cross-target protocol-seam package
|