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
|