Skip to content

Commit e1541c3

Browse files
ampagenttmm
andcommitted
docs(swift): DocC catalogs, a README, and the JS parity matrix
A catalog per library product: Zero (getting started, and what each JS option, method, and React hook is called in Swift), ZeroUI, and ZeroTesting. swift/README.md is the mirror's front page, with the compatibility table. scripts/docs.sh builds the catalogs with warnings as errors; CI runs it for Zero and ZeroTesting on Linux, and for all three on macOS (ZeroUI's symbols exist only there). Oxfmt skips the catalogs, since it turns DocC's double-backtick symbol links into code voice, and the recorded drift vectors, like the conformance vectors. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
1 parent 57bf70f commit e1541c3

15 files changed

Lines changed: 696 additions & 13 deletions

File tree

‎.github/workflows/swift-macos.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ jobs:
4242
- run: swift --version
4343
- name: Build and test (macOS)
4444
run: swift test
45+
- name: Docs
46+
run: scripts/docs.sh
4547
- name: Test (iOS Simulator)
4648
run: |
4749
device="$(xcrun simctl list devices available -j |

‎.github/workflows/swift.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ jobs:
7777
run: swift build --build-tests
7878
- name: Test
7979
run: swift test --skip-build
80+
# Broken symbol links and undocumented parameters fail. ZeroUI is
81+
# Apple-only; swift-macos.yml checks it.
82+
- name: Docs
83+
run: scripts/docs.sh Zero ZeroTesting
8084
# zbugs' Swift port, checked against zbugs' TS queries and mutators on
8185
# the samples in drift-samples.ts (the vectors job checks the record is
8286
# current).

‎.oxfmtrc.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"apps/zbugs/db/migrations/",
3434
"**/__snapshots__/**",
3535
"packages/zero-client/src/client/zero-stress-queries-test.ts",
36-
"packages/zql-benchmarks/src/planner-cost.bench.ts"
36+
"packages/zql-benchmarks/src/planner-cost.bench.ts",
37+
"swift/Sources/*/*.docc",
38+
"**/drift-vectors.json"
3739
]
3840
}

‎apps/zbugs-swift/App/project.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ name: ZBugs
55
options:
66
bundleIdPrefix: dev.rocicorp
77
deploymentTarget:
8-
iOS: "26.0"
9-
macOS: "26.0"
8+
iOS: '26.0'
9+
macOS: '26.0'
1010
packages:
1111
ZBugs:
1212
path: ..
@@ -27,13 +27,13 @@ targets:
2727
# Not ZBugs, the package module it imports.
2828
PRODUCT_MODULE_NAME: ZBugsApp
2929
PRODUCT_BUNDLE_IDENTIFIER: dev.rocicorp.zbugs
30-
SWIFT_VERSION: "6.0"
30+
SWIFT_VERSION: '6.0'
3131
GENERATE_INFOPLIST_FILE: YES
3232
INFOPLIST_FILE: Info.plist
3333
INFOPLIST_KEY_CFBundleDisplayName: Zero Bugs
3434
INFOPLIST_KEY_UILaunchScreen_Generation: YES
3535
INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
3636
INFOPLIST_KEY_LSApplicationCategoryType: public.app-category.developer-tools
37-
TARGETED_DEVICE_FAMILY: "1,2"
37+
TARGETED_DEVICE_FAMILY: '1,2'
3838
CODE_SIGN_STYLE: Automatic
3939
ENABLE_USER_SCRIPT_SANDBOXING: YES

‎swift/DESIGN.md‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,16 @@ be lowered later with `@available` shims.
5353
```
5454
swift/ Swift package (source of truth)
5555
Package.swift
56-
Sources/Zero/ core engine, platform-agnostic
57-
Sources/ZeroUI/ SwiftUI, UIKit, AppKit helpers; URLSession transport
56+
README.md the mirror's front page
57+
Sources/Zero/ core engine; on Apple platforms, the URLSession
58+
transport and app lifecycle too
59+
Sources/Zero/Zero.docc/ DocC: getting started, the JS parity matrix
60+
Sources/ZeroUI/ SwiftUI, UIKit, AppKit helpers
5861
Sources/ZeroTesting/ drift-harness replay API and test helpers
5962
Sources/ZeroNIOTesting/ SwiftNIO WebSocket transport for Linux end-to-end tests
6063
Sources/ZeroBenchmarks/ benchmarks (BENCHMARKS.md)
6164
Tests/...
65+
scripts/docs.sh builds the DocC catalogs, failing on warnings
6266
VERSION
6367
packages/zero-conformance/ private JS package: fixtures, vector generators, JS runners
6468
packages/zero-swift-cli/ npm package `zero-swift`: `codegen`, `drift record`
@@ -83,7 +87,9 @@ apps/zbugs-swift/ dogfood app (iOS + macOS)
8387
`@rocicorp` scope) with a peer dependency on `@rocicorp/zero`. It is
8488
versioned with the Swift releases.
8589
- License: Apache-2.0, like this repository.
86-
- Docs: DocC, plus a JS→Swift parity matrix.
90+
- Docs: a DocC catalog per library product, with a getting-started article
91+
and a JS→Swift parity matrix (`Zero.docc/JSParity.md`). Oxfmt skips the
92+
catalogs; it would turn DocC's ` ``Symbol`` ` links into code voice.
8793

8894
### Versioning and releases
8995

@@ -481,12 +487,14 @@ the Zero convention of passing IDs and timestamps as arguments.
481487

482488
- **Linux, every PR.** Swift build, unit tests, and conformance vectors; the
483489
vector and drift-vector staleness checks; `apps/zbugs-swift`'s drift test;
484-
an end-to-end run against a real `zero-cache` and Postgres; and
485-
`apps/zbugs-swift` against zbugs' backend (`server/server.ts`).
490+
an end-to-end run against a real `zero-cache` and Postgres;
491+
`apps/zbugs-swift` against zbugs' backend (`server/server.ts`); and the
492+
`Zero` and `ZeroTesting` docs, where a broken symbol link or an
493+
undocumented parameter fails.
486494
- **macOS, path-filtered to `swift/`, `packages/zero-conformance/`, and
487495
`apps/zbugs-swift/`.** The Apple layer (URLSession transport against a local
488496
mock WebSocket server, SwiftUI, UIKit, and AppKit helpers), iOS simulator
489-
tests, and the zbugs app built for iOS and macOS.
497+
tests, all three DocC catalogs, and the zbugs app built for iOS and macOS.
490498
- **Benchmarks.** Ports of the key JS benchmarks
491499
(`zero-client/src/client/zero.bench.ts`, the shared B-tree and hash
492500
benchmarks) plus Swift benchmarks for query hydration over N rows, poke

‎swift/README.md‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Zero for Swift
2+
3+
A native [Zero](https://zero.rocicorp.dev) client for iOS and macOS. Your
4+
app queries a local replica of the server's data, and the replica stays in
5+
sync. Mutations apply to the replica right away and run again on your
6+
server, whose result wins.
7+
8+
It's a port of the JS client (`@rocicorp/zero`) and behaves the same.
9+
Conformance vectors generated from the JS client test it, and so do
10+
end-to-end tests against `zero-cache` and the zbugs app.
11+
12+
> [!NOTE]
13+
> Before 1.0, minor versions may change the API.
14+
15+
## Requirements
16+
17+
- Swift 6.2
18+
- iOS 26 or macOS 26. The `Zero` module also builds on Linux.
19+
- A `zero-cache` that speaks sync protocol 53 (Zero 1.12 or later)
20+
21+
## Install
22+
23+
```swift
24+
// Package.swift
25+
dependencies: [
26+
.package(url: "https://github.com/wevm/zero-swift", from: "0.1.0"),
27+
],
28+
targets: [
29+
.target(name: "App", dependencies: [
30+
.product(name: "Zero", package: "zero-swift"),
31+
.product(name: "ZeroUI", package: "zero-swift"), // SwiftUI, UIKit, AppKit
32+
]),
33+
.testTarget(name: "AppTests", dependencies: [
34+
.product(name: "ZeroTesting", package: "zero-swift"),
35+
]),
36+
]
37+
```
38+
39+
The `zero-swift` CLI generates Swift from your TypeScript schema, and
40+
records the vectors the drift harness checks your Swift queries and
41+
mutators against:
42+
43+
```sh
44+
npm install --save-dev zero-swift
45+
npx zero-swift codegen
46+
npx zero-swift drift record
47+
```
48+
49+
## Example
50+
51+
```swift
52+
import SwiftUI
53+
import ZeroUI
54+
55+
let zero = try Zero(
56+
schema: .app, context: auth, mutators: [IssueMutators.create],
57+
options: ZeroOptions(cacheURL: "https://zero.example.com", userID: auth?.sub, auth: token))
58+
59+
struct IssueList: View {
60+
@ZeroQuery(IssueQueries.open()) var issues
61+
62+
var body: some View {
63+
List(issues) { Text($0.title) }
64+
}
65+
}
66+
67+
zero.mutate(IssueMutators.create(.init(id: newID(), title: "Crash on launch", created: now)))
68+
```
69+
70+
## Documentation
71+
72+
The DocC catalogs in `Sources/*/*.docc` cover the following. Open them in
73+
Xcode with Product › Build Documentation.
74+
75+
- [Getting started](Sources/Zero/Zero.docc/GettingStarted.md): schema
76+
generation, porting queries and mutators, creating a client.
77+
- [Coming from the JS client](Sources/Zero/Zero.docc/JSParity.md): what
78+
each JS option, method, and React hook is called in Swift, and what isn't
79+
ported.
80+
- [`ZeroUI`](Sources/ZeroUI/ZeroUI.docc/ZeroUI.md): SwiftUI, UIKit, and
81+
AppKit.
82+
- [`ZeroTesting`](Sources/ZeroTesting/ZeroTesting.docc/ZeroTesting.md): the
83+
drift harness and test doubles.
84+
85+
[`DESIGN.md`](DESIGN.md) explains how the port is built and tested, and
86+
[`BENCHMARKS.md`](BENCHMARKS.md) compares it with the JS client.
87+
88+
## Compatibility
89+
90+
| Zero for Swift | Sync protocol | `zero-cache` | Conformance vectors from |
91+
| -------------- | ------------- | ------------ | ------------------------ |
92+
| 0.1 | 53 | 1.12+ | Zero 1.12.0 |
93+
94+
## License
95+
96+
Apache-2.0

‎swift/Sources/Zero/Client/Log.swift‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ public enum LogLevel: Int, Sendable, Comparable, CaseIterable, CustomStringConve
2525

2626
/// Where the client's log messages go (`LogSink` in `@rocicorp/logger`).
2727
public protocol LogSink: Sendable {
28-
/// - Parameter context: Which client logged the message, such as
29-
/// `clientID`.
28+
/// - Parameters:
29+
/// - level: How severe the message is.
30+
/// - message: The message, without the context.
31+
/// - context: Which client logged the message, such as `clientID`.
3032
func log(level: LogLevel, message: String, context: [String: String])
3133
}
3234

‎swift/Sources/Zero/Client/ZeroClient.swift‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ public final class Zero<Context: Sendable> {
2626
#endif
2727

2828
/// - Parameters:
29+
/// - schema: The generated schema, such as `.app`.
30+
/// - context: What queries and mutators are given besides their
31+
/// arguments, such as the logged-in user's auth data.
2932
/// - mutators: Every mutator the client may run. Pending mutations are
3033
/// replayed by name, so each must be registered.
34+
/// - options: Where to sync from, and how.
3135
/// - scheduler: The clock and timers. With the system clock, the client
3236
/// also follows the app's lifecycle and the network; tests pass a
3337
/// manual one.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Getting started
2+
3+
Generate your schema, port the queries and mutators your app uses, and
4+
create a client.
5+
6+
## Add the package
7+
8+
The package is published from this repository's `swift/` directory to
9+
`github.com/wevm/zero-swift`:
10+
11+
```swift
12+
.package(url: "https://github.com/wevm/zero-swift", from: "0.1.0"),
13+
```
14+
15+
Depend on `Zero`, and on `ZeroUI` for SwiftUI, UIKit, and AppKit. It needs
16+
Swift 6.2 and iOS 26 or macOS 26; the core also builds on Linux.
17+
18+
## Generate the schema
19+
20+
Your TypeScript schema stays the source of truth. The `zero-swift` CLI
21+
generates Swift from it: a ``ZeroSchema``, a row struct for each table, and
22+
a `zql` builder with a ``Query`` for each table.
23+
24+
```json
25+
{
26+
"schema": "../web/shared/schema.ts",
27+
"output": "Sources/App/Schema.swift",
28+
"schemaName": "app",
29+
"tables": {
30+
"issue": {"columns": {"shortID": {"type": "int"}}}
31+
}
32+
}
33+
```
34+
35+
```sh
36+
npx zero-swift codegen # reads zero-swift.json
37+
npx zero-swift codegen --check # in CI: fails when Schema.swift is stale
38+
```
39+
40+
`number()` columns are `Double`; give a column the type `int` to make it an
41+
`Int`, which must then hold integers. `json<T>()` columns get a generated `Codable` type when `T` is a
42+
plain object or literal union.
43+
44+
## Port your queries and mutators
45+
46+
Write the queries and mutators your app runs in Swift, named as in your
47+
TypeScript registries. The server runs the TypeScript ones; the client runs
48+
the Swift ones to show results before the server answers.
49+
50+
```swift
51+
enum IssueQueries {
52+
static let open = defineQuery("issues.open") { (_: NoArgs, auth: AuthData?) in
53+
zql.issue.where(\.open, true).orderBy(\.modified, .desc).limit(50)
54+
}
55+
static let byID = defineQuery("issues.byID") { (id: String, auth: AuthData?) in
56+
zql.issue.where(\.id, id).related(\.comments).one()
57+
}
58+
}
59+
60+
enum IssueMutators {
61+
struct Create: Codable, Sendable {
62+
var id: String
63+
var title: String
64+
var created: Double
65+
}
66+
67+
static let create = defineMutator("issue.create") { (tx, args: Create, auth: AuthData?) in
68+
guard let auth else { throw ApplicationError("Not logged in") }
69+
try tx.mutate.issue.insert(
70+
Issue(id: args.id, title: args.title, creatorID: auth.sub, created: args.created))
71+
}
72+
}
73+
```
74+
75+
Mutators are plain `throws` functions: they read and write through the
76+
``Transaction`` synchronously. Pass IDs and timestamps in as arguments
77+
rather than making them up inside, so the client and the server write the
78+
same rows.
79+
80+
The drift harness checks the ports: `zero-swift drift record` runs your
81+
TypeScript queries and mutators on sample arguments and writes what they
82+
did, and `Drift.expectNoDrift` in `ZeroTesting` fails a test where the Swift
83+
ones do something else, or where a query or mutator exists on one side only.
84+
85+
## Create a client
86+
87+
```swift
88+
var options = ZeroOptions(cacheURL: "https://zero.example.com", userID: auth?.sub, auth: token)
89+
options.mutateURL = "https://api.example.com/mutate" // or zero-cache's configured URL
90+
options.queryURL = "https://api.example.com/query"
91+
92+
let zero = try Zero(
93+
schema: .app, context: auth, mutators: [IssueMutators.create], options: options)
94+
```
95+
96+
Register every mutator the app may run: pending mutations are stored by
97+
name and run again when the server's state arrives, including after a
98+
relaunch. The client keeps its replica in SQLite (``ZeroStorage``) and
99+
connects with `URLSession`.
100+
101+
## Read rows
102+
103+
``LiveQuery`` is an observable object whose `data` stays current:
104+
105+
```swift
106+
let open = try zero.liveQuery(IssueQueries.open())
107+
open.data // [Issue]
108+
open.resultType // .unknown until the server has confirmed the rows, then .complete
109+
```
110+
111+
In SwiftUI, `@ZeroQuery` from `ZeroUI` does the same for a view. For UIKit
112+
and AppKit, `DiffableAdapter` feeds a diffable data source.
113+
114+
`zero.materialize(_:ttl:)` returns the untyped ``MaterializedView``, whose
115+
``ViewSnapshot``s say which rows were inserted, removed, or changed.
116+
`zero.run(_:type:ttl:)` reads rows once, and `zero.preload(_:ttl:)` syncs
117+
rows without a view.
118+
119+
## Change rows
120+
121+
```swift
122+
let result = zero.mutate(IssueMutators.create(.init(id: newID(), title: "Crash", created: now)))
123+
try await result.client.value // the optimistic run
124+
try await result.server.value // the server's verdict
125+
```
126+
127+
The client applies the mutator at once; views update before `mutate`
128+
returns. The server runs its own mutator; when its result arrives, the
129+
client replaces its optimistic changes with the server's.
130+
131+
## Follow the connection
132+
133+
``Connection`` is observable. Its ``ConnectionState`` is `.needsAuth` when
134+
the server rejects the token: get a new one and call
135+
``Connection/connect(auth:)``.

0 commit comments

Comments
 (0)