Skip to content
Web apps · Workflow

SwiftUI with a Swift package core tested by swift test in CI

SwiftUI screens as thin views over @Observable view models in a Swift package, tested with Swift Testing by swift test on Linux and macOS CI.

Verified install Not agent-tested v1.0.0 MIT updated swiftswiftuiiosgithub-actions
Equips
  • 1 1 Rules
  • 1 1 Skills
  • – 0 MCP

Agents writing SwiftUI put the logic in body, where nothing can test it, and then claim the feature works because Xcode built. This loadout gives your agent a structure that can be tested. Each screen's state, validation and side effects live in a main-actor @Observable view model inside a local Swift package. The network and clock come in through Sendable protocols, so tests swap in actor fakes. The SwiftUI view stays thin and sits behind canImport(SwiftUI), so the package builds and tests on Linux too. A cheap Linux CI job runs swift format lint and swift test in the Swift 6.4.1 container. A macOS job builds the views for the iOS simulator. The verifier runs the full example in the Swift 6 language mode on Swift 6.4.1: six tests and nine cases pass. It also checks that a swallowed error fails the tests and that a main-actor violation fails the build.

What's in it

  • @MainActor @Observable view model with enum state and no SwiftUI import
  • Injected Sendable client and clock; actor fakes in tests
  • Swift Testing with parameterised cases instead of XCTest copies
  • Linux CI runs swift test; macOS CI compiles the views for the simulator
  • Verified on Swift 6.4.1 in the Swift 6 language mode

You'll need

  • Tools: swift
Level 1 · automated

Verification

Verified install

Checked at commit 08c3710 · 9 passed, 0 failed, 1 not applicable

CheckResultWhat we found
Listinglisting.yaml matches the schema and declares every file in the folder. Pass
Schema valid; 11 files declared, none missing or extra
  • tested.status: untested (agent tests are separate from Level 1)
Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. Pass All 11 files parse; front matter valid
AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. Pass
CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
  • CLAUDE.md: matches the generator
  • GEMINI.md: matches the generator
Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. Pass
Correct paths for all 6 agents; layouts re-discovered
  • Claude Code: 10 files, 1 skill in .claude/skills/
  • Codex CLI: 9 files, 1 skill in .agents/skills/
  • Cursor: 9 files, 1 skill in .agents/skills/
  • Gemini CLI: 10 files, 1 skill in .agents/skills/
  • Antigravity CLI: 9 files, 1 skill in .agents/skills/
  • GitHub Copilot: 9 files, 1 skill in .agents/skills/
MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. Not applicable No MCP servers in this loadout
Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode or prompt-injection wording, and the listing's network flag is honest. Pass
Clean: no credentials, risky shell or hidden text
  • Scanner intake-precheck/1: verdict clean; network no (listing says no)
ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. Pass 3 references between files resolve for every agent
Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. Pass
11 files, 16.0 KB total; largest skills/swiftui-testable-feature/references/NotesModelTests.swift 3.7 KB
  • Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule
LicenceA recognised SPDX licence, with the licence text available. Pass MIT, by Agent Loadout; text in the repository LICENSE
Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. Pass
The swiftui-testable-feature skill's package (view model, models, #if-guarded SwiftUI view, Swift Testing suite) builds in the Swift 6 language mode and passes swift format lint --strict and swift test (6 tests, 9 cases) on Linux; a swallowed error fails the tests and a main-actor violation fails th
  • Applied 19 loadout files at every agent's paths
  • ✓ place the skill's package: bash -c R=.agents/skills/swiftui-testable-feature/references; P=Packages/NotesCore; mkdir -p $P/Sources/NotesCore $P/Tests/NotesCoreTests && cp $R/Package.swift $P/ && cp $R/Note.swift $R/NotesModel.swift $R/NotesView.swift $P/Sources/NotesCore/ && cp $R/NotesModelTests.swift $P/Tests/NotesCoreTests/ && cp $R/swift-format.json $P/.swift-format && echo placed
  • ✓ swift format lint --strict: bash -c cd Packages/NotesCore && swift format lint --strict --recursive Sources Tests Package.swift && echo lint clean
  • ✓ swift build --build-tests (Swift 6 language mode): bash -c cd Packages/NotesCore && swift build --build-tests 2>&1
  • ✓ swift test: bash -c cd Packages/NotesCore && swift test --parallel 2>&1
  • ✓ a swallowed load error fails the tests: bash -c rm -rf m && mkdir m && cp -r Packages/NotesCore/Package.swift Packages/NotesCore/Sources Packages/NotesCore/Tests m/ && sed -i 's/ state = .failed(Self.message(for: error))/ state = .loaded([])/' m/Sources/NotesCore/NotesModel.swift && grep -q 'state = .loaded(\[\])' m/Sources/NotesCore/NotesModel.swift && cd m && swift test 2>&1; test $? -ne 0 && echo caught
  • ✓ mutating main-actor state from a detached task fails the build: bash -c cd m && cp ../Packages/NotesCore/Sources/NotesCore/NotesModel.swift Sources/NotesCore/ && sed -i 's/^ public func load() async {/ public func refreshInBackground() {\n Task.detached { self.state = .loading }\n }\n\n public func load() async {/' Sources/NotesCore/NotesModel.swift && grep -q 'Task.detached' Sources/NotesCore/NotesModel.swift && swift build 2>&1; test $? -ne 0 && echo refused
  • Covers: @MainActor @Observable view model with enum state, injected Sendable client and clock
  • Covers: SwiftUI view guarded by #if canImport(SwiftUI) so the package builds and tests on Linux
  • Covers: Swift Testing suite with parameterised cases and actor fakes
  • Covers: Swift 6 strict concurrency enforced; swift format lint --strict

What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results ("tested with agent") are separate and stay untested until an agent actually runs.

Install

Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.

Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.

Download zip
curl -fsSL --create-dirs -o AGENTS.md https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/AGENTS.md
curl -fsSL --create-dirs -o CLAUDE.md https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/CLAUDE.md
curl -fsSL --create-dirs -o .claude/skills/swiftui-testable-feature/SKILL.md https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/skills/swiftui-testable-feature/SKILL.md
curl -fsSL --create-dirs -o .claude/skills/swiftui-testable-feature/references/Package.swift https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/skills/swiftui-testable-feature/references/Package.swift
curl -fsSL --create-dirs -o .claude/skills/swiftui-testable-feature/references/Note.swift https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/skills/swiftui-testable-feature/references/Note.swift
curl -fsSL --create-dirs -o .claude/skills/swiftui-testable-feature/references/NotesModel.swift https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/skills/swiftui-testable-feature/references/NotesModel.swift
curl -fsSL --create-dirs -o .claude/skills/swiftui-testable-feature/references/NotesView.swift https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/skills/swiftui-testable-feature/references/NotesView.swift
curl -fsSL --create-dirs -o .claude/skills/swiftui-testable-feature/references/NotesModelTests.swift https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/skills/swiftui-testable-feature/references/NotesModelTests.swift
curl -fsSL --create-dirs -o .claude/skills/swiftui-testable-feature/references/swift-format.json https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/skills/swiftui-testable-feature/references/swift-format.json
curl -fsSL --create-dirs -o .claude/skills/swiftui-testable-feature/references/swift-ci.yml https://loadout.jnw-it.com/raw/swiftui-swift-test-ci/1.0.0/skills/swiftui-testable-feature/references/swift-ci.yml
FileGoes to
AGENTS.mdAGENTS.md
CLAUDE.mdCLAUDE.md
skills/swiftui-testable-feature/SKILL.md.claude/skills/swiftui-testable-feature/SKILL.md
skills/swiftui-testable-feature/references/Package.swift.claude/skills/swiftui-testable-feature/references/Package.swift
skills/swiftui-testable-feature/references/Note.swift.claude/skills/swiftui-testable-feature/references/Note.swift
skills/swiftui-testable-feature/references/NotesModel.swift.claude/skills/swiftui-testable-feature/references/NotesModel.swift
skills/swiftui-testable-feature/references/NotesView.swift.claude/skills/swiftui-testable-feature/references/NotesView.swift
skills/swiftui-testable-feature/references/NotesModelTests.swift.claude/skills/swiftui-testable-feature/references/NotesModelTests.swift
skills/swiftui-testable-feature/references/swift-format.json.claude/skills/swiftui-testable-feature/references/swift-format.json
skills/swiftui-testable-feature/references/swift-ci.yml.claude/skills/swiftui-testable-feature/references/swift-ci.yml

Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.

Files

Rules AGENTS.md Rules for a SwiftUI app whose view models live in a Swift package tested by swift test. 24 lines
Download Raw
# AGENTS.md: SwiftUI app with a tested Swift package core

An iOS and macOS app in SwiftUI. Screens are thin views over `@Observable` view models that live in a local Swift
package (`Packages/`), built in the Swift 6 language mode and tested with Swift Testing. Check `Package.swift` for
tools version and platforms before using an API.

## Commands (run in the package folder)
- Lint: `swift format lint --strict --recursive Sources Tests Package.swift`. Fix: `swift format --in-place --recursive Sources Tests`.
- Build and test: `swift build --build-tests` then `swift test --parallel`. This works on Linux and macOS.
- Views (macOS only): `xcodebuild -scheme <Package> -destination 'generic/platform=iOS Simulator' build`.
- Done means: lint clean, `swift test` green with a test for the behaviour you changed, and the summary says
  whether the views were compiled.

## Rules
- Use the `swiftui-testable-feature` skill when adding a screen or changing view logic.
- View models are `@MainActor @Observable final class` with `private(set)` state and no SwiftUI import.
  Logic lives there, not in `body`.
- Side effects (network, disk, clock) go through `Sendable` protocols injected in `init`. Tests use actor fakes.
- New tests use Swift Testing (`@Test`, `#expect`), with `arguments:` for input tables.
- Fix strict concurrency errors properly. Never add `@unchecked Sendable`, `nonisolated(unsafe)` or
  `@preconcurrency` to silence them.
- No force unwraps or `try!` outside tests. No new package dependencies without saying so.
- Do not change signing, entitlements, bundle identifiers or the Xcode project's build settings unless asked.
Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
Download Raw
# CLAUDE.md

@AGENTS.md

## Claude Code notes

- Skills from this loadout live in `.claude/skills/`: `swiftui-testable-feature`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From the Agent Loadout "SwiftUI with a Swift package core tested by swift test in CI" v1.0.0 — https://loadout.jnw-it.com/loadouts/swiftui-swift-test-ci/ -->
Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
Download Raw
# GEMINI.md

@./AGENTS.md

## Gemini CLI notes

- Skills from this loadout live in `.agents/skills/`: `swiftui-testable-feature`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From the Agent Loadout "SwiftUI with a Swift package core tested by swift test in CI" v1.0.0 — https://loadout.jnw-it.com/loadouts/swiftui-swift-test-ci/ -->
Skill skills/swiftui-testable-feature/SKILL.md Layout, view model and view rules, tests and commands. 76 lines
Download Raw
---
name: swiftui-testable-feature
description: Build a SwiftUI feature with its state and logic in a Swift package view model that swift test covers on Linux and macOS, with a thin view on top. Use when adding a screen, changing view logic, or writing Swift tests.
---

# SwiftUI feature with a testable core

SwiftUI views are hard to test and only build on Apple platforms. So the view stays thin, and everything worth
testing lives in an `@Observable` view model inside a Swift package that builds and tests anywhere, including
a cheap Linux CI runner.

## Layout

```
Packages/NotesCore/
  Package.swift                      swift-tools-version 6.2, Swift 6 language mode
  Sources/NotesCore/Note.swift       value types + the client protocol (Sendable)
  Sources/NotesCore/NotesModel.swift @MainActor @Observable view model, no SwiftUI import
  Sources/NotesCore/NotesView.swift  the view, wrapped in #if canImport(SwiftUI)
  Tests/NotesCoreTests/...           Swift Testing (@Suite, @Test, #expect)
```

The app target (Xcode project) depends on the package and only creates the model with the real client.

## Rules for the view model

- `@MainActor @Observable final class`, with `private(set)` state. The view reads it and calls methods.
- State is an enum (`idle`, `loading`, `loaded`, `failed(message)`), not several booleans that can disagree.
- Every side effect goes through a `Sendable` protocol passed into `init` (network, disk, clock). Time comes
  from an injected `now` closure, so tests are deterministic.
- Derived values (sorting, filtering, validation) are computed properties or methods on the model, not code
  in `body`.
- Errors become a user-facing message in state. Never swallow them into an empty list.
- No `import SwiftUI`, `UIKit` or `AppKit` in the model file.

## Rules for the view

- Wrap the file in `#if canImport(SwiftUI)` so the package still builds on Linux.
- Own the model with `@State`, pass it down as a plain property, bind with `$model.field`.
- Load in `.task { await model.load() }`, refresh with `.refreshable`.
- No business logic in the view: if you want to test it, move it into the model.

## Tests

- Swift Testing, not XCTest, for new tests: `@Suite`, `@Test("sentence")`, `#expect`, and
  `#expect(throws:)`. Use `arguments:` for tables of inputs instead of copy-pasted tests.
- Mark the suite `@MainActor` to call the model directly.
- Fakes are `actor`s that implement the client protocol and record calls. No mocking frameworks.
- Each behaviour has a test: happy path, each error, edge input (blank, too long, whitespace), and that
  invalid input never reaches the client.

## Commands (from the package folder)

```bash
swift format lint --strict --recursive Sources Tests Package.swift   # config: references/swift-format.json as .swift-format
swift build --build-tests
swift test --parallel
```

On a Mac, also build for the simulator so the views compile:
`xcodebuild -scheme NotesCore -destination 'generic/platform=iOS Simulator' build`.
CI runs both (see `references/swift-ci.yml`): Linux in the `swift:6.4.1-noble` container, macOS with Xcode 26.6.

## Done means

- Lint is clean and `swift test` passes, with a new or changed `@Test` for the behaviour you touched.
- Strict concurrency errors are fixed, not silenced: no `@unchecked Sendable`, `nonisolated(unsafe)` or
  `@preconcurrency` added to get a build through.
- The summary says whether the views were built on macOS or only the core was tested on Linux.

## References

`references/Package.swift`, `references/Note.swift`, `references/NotesModel.swift`,
`references/NotesView.swift`, `references/NotesModelTests.swift` (six tests, nine cases),
`references/swift-format.json`, `references/swift-ci.yml`.
Skill file skills/swiftui-testable-feature/references/Package.swift Swift 6 language mode package for iOS 18 and macOS 15. 16 lines
Download Raw
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "NotesCore",
    platforms: [.iOS(.v18), .macOS(.v15)],
    products: [
        .library(name: "NotesCore", targets: ["NotesCore"])
    ],
    targets: [
        .target(name: "NotesCore"),
        .testTarget(name: "NotesCoreTests", dependencies: ["NotesCore"]),
    ],
    swiftLanguageModes: [.v6]
)
Skill file skills/swiftui-testable-feature/references/Note.swift Value type, client protocol and errors. 28 lines
Download Raw
import Foundation

public struct Note: Identifiable, Equatable, Sendable, Codable {
    public let id: UUID
    public var title: String
    public var body: String
    public var updatedAt: Date

    public init(id: UUID = UUID(), title: String, body: String, updatedAt: Date) {
        self.id = id
        self.title = title
        self.body = body
        self.updatedAt = updatedAt
    }
}

/// The only way the view model reaches the network or disk. Tests pass a fake.
public protocol NotesClient: Sendable {
    func fetchNotes() async throws -> [Note]
    func save(_ note: Note) async throws -> Note
}

public enum NotesError: Error, Equatable, Sendable {
    case offline
    case server(status: Int)
    case invalidTitle
}
Skill file skills/swiftui-testable-feature/references/NotesModel.swift @MainActor @Observable view model. 72 lines
Download Raw
import Foundation
import Observation

/// View state for the notes screen. Owns no SwiftUI types, so it builds and tests on Linux.
@MainActor
@Observable
public final class NotesModel {
    public enum State: Equatable {
        case idle
        case loading
        case loaded([Note])
        case failed(String)
    }

    public private(set) var state: State = .idle
    public var searchText = ""

    private let client: any NotesClient
    private let now: @Sendable () -> Date

    public init(client: any NotesClient, now: @escaping @Sendable () -> Date = Date.init) {
        self.client = client
        self.now = now
    }

    /// Notes to show: newest first, filtered by the search text (title or body, case-insensitive).
    public var visibleNotes: [Note] {
        guard case .loaded(let notes) = state else { return [] }
        let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
        let filtered =
            query.isEmpty
            ? notes
            : notes.filter { $0.title.localizedCaseInsensitiveContains(query) || $0.body.localizedCaseInsensitiveContains(query) }
        return filtered.sorted { $0.updatedAt > $1.updatedAt }
    }

    public func load() async {
        if case .loading = state { return }
        state = .loading
        do {
            state = .loaded(try await client.fetchNotes())
        } catch {
            state = .failed(Self.message(for: error))
        }
    }

    /// Validates, saves through the client, and replaces or appends the saved note.
    public func save(title: String, body: String, id: UUID? = nil) async throws {
        let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty, trimmed.count <= 120 else { throw NotesError.invalidTitle }
        let saved = try await client.save(Note(id: id ?? UUID(), title: trimmed, body: body, updatedAt: now()))
        guard case .loaded(var notes) = state else {
            state = .loaded([saved])
            return
        }
        if let index = notes.firstIndex(where: { $0.id == saved.id }) {
            notes[index] = saved
        } else {
            notes.append(saved)
        }
        state = .loaded(notes)
    }

    static func message(for error: any Error) -> String {
        switch error {
        case NotesError.offline: "You're offline. Pull to try again."
        case NotesError.server(let status): "The server returned an error (\(status))."
        default: "Something went wrong."
        }
    }
}
Skill file skills/swiftui-testable-feature/references/NotesView.swift Thin SwiftUI view behind canImport(SwiftUI). 39 lines
Download Raw
#if canImport(SwiftUI)
import SwiftUI

/// Thin view: reads model state, forwards user intent. No logic worth testing lives here.
public struct NotesView: View {
    @State private var model: NotesModel

    public init(model: NotesModel) {
        _model = State(initialValue: model)
    }

    public var body: some View {
        NavigationStack {
            content
                .navigationTitle("Notes")
                .searchable(text: $model.searchText)
                .refreshable { await model.load() }
                .task { await model.load() }
        }
    }

    @ViewBuilder private var content: some View {
        switch model.state {
        case .idle, .loading:
            ProgressView()
        case .failed(let message):
            ContentUnavailableView("Couldn't load notes", systemImage: "wifi.slash", description: Text(message))
        case .loaded:
            List(model.visibleNotes) { note in
                VStack(alignment: .leading) {
                    Text(note.title).font(.headline)
                    Text(note.body).font(.subheadline).lineLimit(2)
                }
            }
        }
    }
}
#endif
Skill file skills/swiftui-testable-feature/references/NotesModelTests.swift Swift Testing suite with actor fakes. 102 lines
Download Raw
import Foundation
import Testing

@testable import NotesCore

/// Fake client: canned results, records saves. An actor, so it is Sendable without locks.
actor FakeNotesClient: NotesClient {
    var notes: [Note]
    var fetchError: NotesError?
    private(set) var saved: [Note] = []

    init(notes: [Note] = [], fetchError: NotesError? = nil) {
        self.notes = notes
        self.fetchError = fetchError
    }

    func fetchNotes() async throws -> [Note] {
        if let fetchError { throw fetchError }
        return notes
    }

    func save(_ note: Note) async throws -> Note {
        saved.append(note)
        return note
    }
}

let day: TimeInterval = 86_400
let fixed = Date(timeIntervalSince1970: 1_790_000_000)

func note(_ title: String, _ body: String = "", daysAgo: Double) -> Note {
    Note(title: title, body: body, updatedAt: fixed.addingTimeInterval(-daysAgo * day))
}

@MainActor
@Suite("NotesModel")
struct NotesModelTests {
    @Test("load shows notes newest first")
    func loadSortsNewestFirst() async {
        let client = FakeNotesClient(notes: [note("Old", daysAgo: 3), note("New", daysAgo: 0), note("Mid", daysAgo: 1)])
        let model = NotesModel(client: client, now: { fixed })
        await model.load()
        #expect(model.visibleNotes.map(\.title) == ["New", "Mid", "Old"])
    }

    @Test(
        "a failed load shows a message, not an empty list",
        arguments: [
            (NotesError.offline, "You're offline. Pull to try again."),
            (NotesError.server(status: 503), "The server returned an error (503)."),
        ])
    func loadFailure(error: NotesError, message: String) async {
        let model = NotesModel(client: FakeNotesClient(fetchError: error), now: { fixed })
        await model.load()
        #expect(model.state == .failed(message))
        #expect(model.visibleNotes.isEmpty)
    }

    @Test("search matches title or body, case-insensitively, ignoring spaces")
    func search() async {
        let client = FakeNotesClient(notes: [
            note("Groceries", "milk, eggs", daysAgo: 0), note("Ideas", "Buy MILK frother", daysAgo: 1), note("Trip", daysAgo: 2),
        ])
        let model = NotesModel(client: client, now: { fixed })
        await model.load()
        model.searchText = "  milk "
        #expect(model.visibleNotes.map(\.title) == ["Groceries", "Ideas"])
    }

    @Test("save trims the title, stamps the time and appends")
    func saveAppends() async throws {
        let client = FakeNotesClient(notes: [note("First", daysAgo: 1)])
        let model = NotesModel(client: client, now: { fixed })
        await model.load()
        try await model.save(title: "  Second  ", body: "b")
        #expect(model.visibleNotes.map(\.title) == ["Second", "First"])
        let saved = await client.saved
        #expect(saved.count == 1)
        #expect(saved.first?.updatedAt == fixed)
    }

    @Test("save with an existing id replaces that note")
    func saveReplaces() async throws {
        let existing = note("Draft", daysAgo: 2)
        let model = NotesModel(client: FakeNotesClient(notes: [existing]), now: { fixed })
        await model.load()
        try await model.save(title: "Final", body: "done", id: existing.id)
        #expect(model.visibleNotes.count == 1)
        #expect(model.visibleNotes.first?.title == "Final")
    }

    @Test("blank or over-long titles are rejected before the client is called", arguments: ["", "   ", String(repeating: "x", count: 121)])
    func invalidTitle(title: String) async {
        let client = FakeNotesClient()
        let model = NotesModel(client: client, now: { fixed })
        await #expect(throws: NotesError.invalidTitle) {
            try await model.save(title: title, body: "")
        }
        #expect(await client.saved.isEmpty)
    }
}
Skill file skills/swiftui-testable-feature/references/swift-format.json swift-format config, saved as .swift-format. 15 lines
Download Raw
{
  "version": 1,
  "indentation": { "spaces": 4 },
  "lineLength": 140,
  "indentConditionalCompilationBlocks": false,
  "rules": {
    "AllPublicDeclarationsHaveDocumentation": false,
    "NeverForceUnwrap": true,
    "NeverUseForceTry": true,
    "NeverUseImplicitlyUnwrappedOptionals": true,
    "OrderedImports": true,
    "UseLetInEveryBoundCaseVariable": true
  }
}
Skill file skills/swiftui-testable-feature/references/swift-ci.yml Linux swift test job and macOS simulator build job. 40 lines
Download Raw
# .github/workflows/swift.yml
# Linux: format lint and swift test for the core package (fast, cheap, no Xcode).
# macOS: build the package for macOS and the iOS simulator, so the SwiftUI views compile too.
name: Swift

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  linux:
    runs-on: ubuntu-24.04
    container: swift:6.4.1-noble
    timeout-minutes: 15
    defaults:
      run:
        working-directory: Packages/NotesCore
    steps:
      - uses: actions/checkout@v7
      - run: swift format lint --strict --recursive Sources Tests Package.swift
      - run: swift build --build-tests
      - run: swift test --parallel

  macos:
    runs-on: macos-26
    timeout-minutes: 30
    defaults:
      run:
        working-directory: Packages/NotesCore
    steps:
      - uses: actions/checkout@v7
      - run: sudo xcode-select -s /Applications/Xcode_26.6.app
      - run: swift test --parallel
      - name: Build for the iOS simulator (compiles the SwiftUI views)
        run: xcodebuild -scheme NotesCore -destination 'generic/platform=iOS Simulator' -skipPackagePluginValidation build

All files: source zip · Licence MIT · By Agent Loadout · First listed

Installing and testing

How do I install SwiftUI with a Swift package core tested by swift test in CI in Claude Code?

From your project root, put AGENTS.md, CLAUDE.md, skills/swiftui-testable-feature/SKILL.md at .claude/skills/swiftui-testable-feature/SKILL.md, skills/swiftui-testable-feature/references/Package.swift at .claude/skills/swiftui-testable-feature/references/Package.swift, skills/swiftui-testable-feature/references/Note.swift at .claude/skills/swiftui-testable-feature/references/Note.swift, skills/swiftui-testable-feature/references/NotesModel.swift at .claude/skills/swiftui-testable-feature/references/NotesModel.swift, skills/swiftui-testable-feature/references/NotesView.swift at .claude/skills/swiftui-testable-feature/references/NotesView.swift, skills/swiftui-testable-feature/references/NotesModelTests.swift at .claude/skills/swiftui-testable-feature/references/NotesModelTests.swift, skills/swiftui-testable-feature/references/swift-format.json at .claude/skills/swiftui-testable-feature/references/swift-format.json, skills/swiftui-testable-feature/references/swift-ci.yml at .claude/skills/swiftui-testable-feature/references/swift-ci.yml. The "Claude Code" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/swiftui-swift-test-ci/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SwiftUI with a Swift package core tested by swift test in CI in Codex CLI?

From your project root, put AGENTS.md, skills/swiftui-testable-feature/SKILL.md at .agents/skills/swiftui-testable-feature/SKILL.md, skills/swiftui-testable-feature/references/Package.swift at .agents/skills/swiftui-testable-feature/references/Package.swift, skills/swiftui-testable-feature/references/Note.swift at .agents/skills/swiftui-testable-feature/references/Note.swift, skills/swiftui-testable-feature/references/NotesModel.swift at .agents/skills/swiftui-testable-feature/references/NotesModel.swift, skills/swiftui-testable-feature/references/NotesView.swift at .agents/skills/swiftui-testable-feature/references/NotesView.swift, skills/swiftui-testable-feature/references/NotesModelTests.swift at .agents/skills/swiftui-testable-feature/references/NotesModelTests.swift, skills/swiftui-testable-feature/references/swift-format.json at .agents/skills/swiftui-testable-feature/references/swift-format.json, skills/swiftui-testable-feature/references/swift-ci.yml at .agents/skills/swiftui-testable-feature/references/swift-ci.yml. The "Codex CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/swiftui-swift-test-ci/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SwiftUI with a Swift package core tested by swift test in CI in Cursor?

From your project root, put AGENTS.md, skills/swiftui-testable-feature/SKILL.md at .agents/skills/swiftui-testable-feature/SKILL.md, skills/swiftui-testable-feature/references/Package.swift at .agents/skills/swiftui-testable-feature/references/Package.swift, skills/swiftui-testable-feature/references/Note.swift at .agents/skills/swiftui-testable-feature/references/Note.swift, skills/swiftui-testable-feature/references/NotesModel.swift at .agents/skills/swiftui-testable-feature/references/NotesModel.swift, skills/swiftui-testable-feature/references/NotesView.swift at .agents/skills/swiftui-testable-feature/references/NotesView.swift, skills/swiftui-testable-feature/references/NotesModelTests.swift at .agents/skills/swiftui-testable-feature/references/NotesModelTests.swift, skills/swiftui-testable-feature/references/swift-format.json at .agents/skills/swiftui-testable-feature/references/swift-format.json, skills/swiftui-testable-feature/references/swift-ci.yml at .agents/skills/swiftui-testable-feature/references/swift-ci.yml. The "Cursor" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/swiftui-swift-test-ci/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SwiftUI with a Swift package core tested by swift test in CI in Gemini CLI?

From your project root, put AGENTS.md, GEMINI.md, skills/swiftui-testable-feature/SKILL.md at .agents/skills/swiftui-testable-feature/SKILL.md, skills/swiftui-testable-feature/references/Package.swift at .agents/skills/swiftui-testable-feature/references/Package.swift, skills/swiftui-testable-feature/references/Note.swift at .agents/skills/swiftui-testable-feature/references/Note.swift, skills/swiftui-testable-feature/references/NotesModel.swift at .agents/skills/swiftui-testable-feature/references/NotesModel.swift, skills/swiftui-testable-feature/references/NotesView.swift at .agents/skills/swiftui-testable-feature/references/NotesView.swift, skills/swiftui-testable-feature/references/NotesModelTests.swift at .agents/skills/swiftui-testable-feature/references/NotesModelTests.swift, skills/swiftui-testable-feature/references/swift-format.json at .agents/skills/swiftui-testable-feature/references/swift-format.json, skills/swiftui-testable-feature/references/swift-ci.yml at .agents/skills/swiftui-testable-feature/references/swift-ci.yml. The "Gemini CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/swiftui-swift-test-ci/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SwiftUI with a Swift package core tested by swift test in CI in Antigravity CLI?

From your project root, put AGENTS.md, skills/swiftui-testable-feature/SKILL.md at .agents/skills/swiftui-testable-feature/SKILL.md, skills/swiftui-testable-feature/references/Package.swift at .agents/skills/swiftui-testable-feature/references/Package.swift, skills/swiftui-testable-feature/references/Note.swift at .agents/skills/swiftui-testable-feature/references/Note.swift, skills/swiftui-testable-feature/references/NotesModel.swift at .agents/skills/swiftui-testable-feature/references/NotesModel.swift, skills/swiftui-testable-feature/references/NotesView.swift at .agents/skills/swiftui-testable-feature/references/NotesView.swift, skills/swiftui-testable-feature/references/NotesModelTests.swift at .agents/skills/swiftui-testable-feature/references/NotesModelTests.swift, skills/swiftui-testable-feature/references/swift-format.json at .agents/skills/swiftui-testable-feature/references/swift-format.json, skills/swiftui-testable-feature/references/swift-ci.yml at .agents/skills/swiftui-testable-feature/references/swift-ci.yml. The "Antigravity CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/swiftui-swift-test-ci/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SwiftUI with a Swift package core tested by swift test in CI in GitHub Copilot?

From your project root, put AGENTS.md, skills/swiftui-testable-feature/SKILL.md at .agents/skills/swiftui-testable-feature/SKILL.md, skills/swiftui-testable-feature/references/Package.swift at .agents/skills/swiftui-testable-feature/references/Package.swift, skills/swiftui-testable-feature/references/Note.swift at .agents/skills/swiftui-testable-feature/references/Note.swift, skills/swiftui-testable-feature/references/NotesModel.swift at .agents/skills/swiftui-testable-feature/references/NotesModel.swift, skills/swiftui-testable-feature/references/NotesView.swift at .agents/skills/swiftui-testable-feature/references/NotesView.swift, skills/swiftui-testable-feature/references/NotesModelTests.swift at .agents/skills/swiftui-testable-feature/references/NotesModelTests.swift, skills/swiftui-testable-feature/references/swift-format.json at .agents/skills/swiftui-testable-feature/references/swift-format.json, skills/swiftui-testable-feature/references/swift-ci.yml at .agents/skills/swiftui-testable-feature/references/swift-ci.yml. The "GitHub Copilot" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/swiftui-swift-test-ci/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

Has SwiftUI with a Swift package core tested by swift test in CI been tested?

It passed the Level 1 automated install checks on 2026-10-17: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.

More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.