Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v3
- run: mise exec -- tuist test
# Pin the iOS Simulator so hosted tests and signing stay predictable (all test targets are iOS).
- run: mise exec -- tuist test --no-selective-testing -- -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2'
19 changes: 15 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
|-------------|---------|--------------|
| Tuist | 4.40.0 | `.mise.toml` |
| SwiftFormat | 0.60.1 | `.mise.toml` |
| Swift PM | 6.2 | `Package.swift` (`swift-tools-version`) |

**Libraries** (**StuffCore**, **WhereCore**, **WhereUI**, **WhereTesting**) are defined in the root [`Package.swift`](Package.swift) (same pattern as Broadway: local package + Tuist for apps and test bundles).

Tuist manifests live at the repo root ([`Project.swift`](Project.swift), [`Tuist.swift`](Tuist.swift)). `Project.swift` references `Package.local(path: .relativeToRoot("."))` and declares the **Where** app, **StuffTestHost**, and unit-test targets that depend on package products.

Tuist manifests live at the repo root (`Project.swift`, `Tuist.swift`).
Run `./ide` (or `./ide -i` to also install dependencies) to regenerate the
Xcode project, install external agent skills, and point Git at `.githooks/`.

Expand Down Expand Up @@ -37,8 +41,9 @@ by `./sync-agents`.

## Targets

- **StuffCore** — macOS framework for shared code (`StuffCore/Sources/`), with unit tests under `StuffCore/Tests/` (Swift Testing).
- Add more targets in `Project.swift` using `macApp()` or `framework()` helpers.
- **Package products** ([`Package.swift`](Package.swift)) — **StuffCore** ([`Shared/StuffCore/Sources/`](Shared/StuffCore/Sources/)), **WhereCore** / **WhereUI** / **WhereTesting** under [`Where/`](Where/).
- **Tuist targets** ([`Project.swift`](Project.swift)) — **Where** app ([`Where/Where/`](Where/Where/)), **StuffTestHost** ([`Shared/StuffTestHost/`](Shared/StuffTestHost/)), **WhereTests** (app tests, no host), and hosted **\*Tests** bundles (**StuffCoreTests**, **WhereCoreTests**, **WhereUITests**) that depend on **StuffTestHost** + **WhereTesting** + the relevant package product.
- Add SPM library targets in `Package.swift` and wire apps/tests in `Project.swift` (see existing `unitTests` helper).

## Deployment

Expand All @@ -49,10 +54,16 @@ by `./sync-agents`.

## Directory layout

Shared code and the shared iOS test host live under **`Shared/`**. Feature apps and their modules (e.g. **Where**) live under a top-level folder per feature (e.g. **`Where/`**).

```
<TargetName>/
Shared/<TargetName>/
Sources/ – production code
Tests/ – unit tests (Swift Testing, not XCTest)

<Feature>/<TargetName>/
Sources/
Tests/
Resources/ – asset catalogs, etc. (apps only)
```

Expand Down
36 changes: 36 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
name: "Stuff",
platforms: [
.iOS(.v26),
],
products: [
.library(name: "StuffCore", targets: ["StuffCore"]),
.library(name: "WhereCore", targets: ["WhereCore"]),
.library(name: "WhereUI", targets: ["WhereUI"]),
.library(name: "WhereTesting", targets: ["WhereTesting"]),
],
targets: [
.target(
name: "StuffCore",
path: "Shared/StuffCore/Sources",
),
.target(
name: "WhereCore",
path: "Where/WhereCore/Sources",
),
.target(
name: "WhereUI",
dependencies: [
.target(name: "WhereCore"),
],
path: "Where/WhereUI/Sources",
),
.target(
name: "WhereTesting",
path: "Where/WhereTesting/Sources",
),
],
)
150 changes: 94 additions & 56 deletions Project.swift
Original file line number Diff line number Diff line change
@@ -1,63 +1,30 @@
import ProjectDescription

let macDestinations: Destinations = [.mac]
let macDeployment: DeploymentTargets = .macOS("26.0")
let destinations: Destinations = [.iPhone, .iPad]
let deployment: DeploymentTargets = .iOS("26.0")

func framework(
_ name: String,
bundleIdSuffix: String,
dependencies: [TargetDependency] = [],
) -> [Target] {
[
.target(
name: name,
destinations: macDestinations,
product: .framework,
bundleId: "com.stuff.\(bundleIdSuffix)",
deploymentTargets: macDeployment,
sources: ["\(name)/Sources/**"],
dependencies: dependencies,
),
.target(
name: "\(name)Tests",
destinations: macDestinations,
product: .unitTests,
bundleId: "com.stuff.\(bundleIdSuffix).tests",
deploymentTargets: macDeployment,
sources: ["\(name)/Tests/**"],
dependencies: [.target(name: name)],
),
]
}
/// Local Swift package (see root `Package.swift`) for StuffCore, WhereCore, WhereUI, and WhereTesting.
private let stuffPackage = Package.local(path: .relativeToRoot("."))

func macApp(
_ name: String,
func unitTests(
name: String,
bundleIdSuffix: String,
infoPlist: [String: Plist.Value] = [:],
dependencies: [TargetDependency] = [],
) -> [Target] {
[
.target(
name: name,
destinations: macDestinations,
product: .app,
bundleId: "com.stuff.\(bundleIdSuffix)",
deploymentTargets: macDeployment,
infoPlist: .extendingDefault(with: infoPlist),
sources: ["\(name)/Sources/**"],
resources: ["\(name)/Resources/**"],
dependencies: dependencies,
),
.target(
name: "\(name)Tests",
destinations: macDestinations,
product: .unitTests,
bundleId: "com.stuff.\(bundleIdSuffix).tests",
deploymentTargets: macDeployment,
sources: ["\(name)/Tests/**"],
dependencies: [.target(name: name)],
),
]
productDependency: String,
sources: ProjectDescription.SourceFilesList,
) -> Target {
.target(
name: name,
destinations: destinations,
product: .unitTests,
bundleId: "com.stuff.\(bundleIdSuffix).tests",
deploymentTargets: deployment,
sources: sources,
dependencies: [
.package(product: productDependency),
.package(product: "WhereTesting"),
.target(name: "StuffTestHost"),
],
)
}

let project = Project(
Expand All @@ -66,5 +33,76 @@ let project = Project(
defaultKnownRegions: ["en"],
developmentRegion: "en",
),
targets: framework("StuffCore", bundleIdSuffix: "stuffcore"),
packages: [stuffPackage],
targets: [
.target(
name: "Where",
destinations: destinations,
product: .app,
bundleId: "com.stuff.where",
deploymentTargets: deployment,
infoPlist: .extendingDefault(with: [
"UILaunchScreen": .dictionary([:]),
"UIApplicationSupportsIndirectInputEvents": .boolean(true),
]),
sources: ["Where/Where/Sources/**"],
resources: ["Where/Where/Resources/**"],
dependencies: [
.package(product: "WhereUI"),
],
),
.target(
name: "WhereTests",
destinations: destinations,
product: .unitTests,
bundleId: "com.stuff.where.tests",
deploymentTargets: deployment,
sources: ["Where/Where/Tests/**"],
dependencies: [
.target(name: "Where"),
.package(product: "WhereTesting"),
],
),
.target(
name: "StuffTestHost",
destinations: destinations,
product: .app,
bundleId: "com.stuff.stufftesthost",
deploymentTargets: deployment,
infoPlist: .extendingDefault(with: [
"UILaunchScreen": .dictionary([:]),
"UIApplicationSceneManifest": .dictionary([
"UIApplicationSupportsMultipleScenes": .boolean(false),
"UISceneConfigurations": .dictionary([
"UIWindowSceneSessionRoleApplication": .array([
.dictionary([
"UISceneConfigurationName": .string("Default Configuration"),
"UISceneDelegateClassName": .string("$(PRODUCT_MODULE_NAME).SceneDelegate"),
]),
]),
]),
]),
]),
sources: ["Shared/StuffTestHost/Sources/**"],
dependencies: [],
),
unitTests(
name: "StuffCoreTests",
bundleIdSuffix: "stuffcore",
productDependency: "StuffCore",
sources: ["Shared/StuffCore/Tests/**"],
),
unitTests(
name: "WhereCoreTests",
bundleIdSuffix: "wherecore",
productDependency: "WhereCore",
sources: ["Where/WhereCore/Tests/**"],
),
unitTests(
name: "WhereUITests",
bundleIdSuffix: "whereui",
productDependency: "WhereUI",
sources: ["Where/WhereUI/Tests/**"],
),
],
)
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ mise install
./ide -i
```

Run tests with `mise exec -- tuist test` (or open the generated workspace in Xcode).
Run tests with `mise exec -- tuist test` (or open the generated workspace in Xcode). CI pins an iOS Simulator destination so the full suite stays consistent.

The `./ide` script sets `core.hooksPath` to `.githooks`. The pre-commit hook
formats staged Swift with SwiftFormat and runs `./sync-agents --git-add` so
Expand All @@ -33,7 +33,8 @@ generated Claude files stay in sync with `AGENTS.md`.
## Project structure

```
Project.swift Tuist project manifest
Package.swift Local Swift package (StuffCore, WhereCore, WhereUI, WhereTesting)
Project.swift Tuist manifest (Where app, StuffTestHost, test bundles → SPM)
Tuist.swift Tuist configuration
.mise.toml Pins Tuist 4.40.0 and SwiftFormat 0.60.1
.swiftformat SwiftFormat rules
Expand All @@ -43,7 +44,9 @@ sync-agents Sync AGENTS.md → CLAUDE.md and .claude/skills/
.githooks/ Git hooks (pre-commit)
.agents/ External skills manifest (`external-skills.json`)
AGENTS.md Repository shape for AI agents
StuffCore/ Shared macOS framework (Sources/, Tests/)
Shared/StuffCore/ Shared iOS framework (Sources/, Tests/)
Shared/StuffTestHost/ Shared iOS unit-test host app (Sources/)
Where/ Where iOS app, modules, and tests
```

## License
Expand Down
16 changes: 16 additions & 0 deletions Shared/StuffTestHost/Sources/SceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import UIKit

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?

func scene(
_ scene: UIScene,
willConnectTo _: UISceneSession,
options _: UIScene.ConnectionOptions,
) {
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = UIViewController()
window?.makeKeyAndVisible()
}
}
17 changes: 17 additions & 0 deletions Shared/StuffTestHost/Sources/TestHostApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import UIKit

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options _: UIScene.ConnectionOptions,
) -> UISceneConfiguration {
let configuration = UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role,
)
configuration.delegateClass = SceneDelegate.self
return configuration
}
}
Empty file added Where/Where/Resources/.gitkeep
Empty file.
11 changes: 11 additions & 0 deletions Where/Where/Sources/WhereApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SwiftUI
import WhereUI

@main
struct WhereApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
6 changes: 6 additions & 0 deletions Where/Where/Tests/WhereTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Testing

@Test
func appModuleLoads() {
#expect(true)
}
4 changes: 4 additions & 0 deletions Where/WhereCore/Sources/WhereCore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public enum WhereCore {
/// Placeholder until location model code lands here.
public static let version = 1
}
7 changes: 7 additions & 0 deletions Where/WhereCore/Tests/WhereCoreTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Testing
import WhereCore

@Test
func versionIsDefined() {
#expect(WhereCore.version == 1)
}
Loading
Loading