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
77 changes: 77 additions & 0 deletions content/docs/(reference)/swift-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,83 @@ A `Reading` is `.list(_:sort:)` or `.search(_:filters:limit:)` for `[Item]`, `.i

`refresh()` reads now and returns after a read that began after the call, so it fits `.refreshable`. On an `.ended` query, it also asks the copy for a new stream. **A query doesn't page**: bound a large list with `ListFilters.limit`.

## Saved views

A `SavedView` is a [`system.view`](/catalog/view) item: a query, and the layouts that show its items. **The working copy answers a view offline**, so every app lists and groups the same items the same way. [Read a view offline](/guides/read-a-view) explains what the copy needs: hydrate `system.view` and every type the view's query names, at the tier the query lists.

```swift
_ = try await copy.hydrate(types: ["system.view", "core.task"], tier: .all)

guard let view = try await copy.items.view(viewID), let layout = view.settings.layouts.first else { return }
let tasks = try await copy.items.list(inView: view.id, layout: layout.id, limit: 50)
let groups = try await copy.items.groups(inView: view.id, layout: layout.id)
for group in groups { print(group.key, group.count) }
```

A layout's `kind` says how it shows items, such as `.board` or `.chart(x:y:)`, and carries the options that kind takes. A `ViewGroup` has the group's `key`, its `count` and, for a chart that sums a property, its `sum`. A live query over a layout reads again when an item enters the view, leaves it or changes group, so a SwiftUI view keeps its counts without refresh code:

```swift
@MainActor @Observable
final class Board {
let groups: LiveQuery<[ViewGroup]>
let tasks: LiveQuery<[Item]>

init(copy: WorkingCopy, viewID: String) {
groups = LiveQuery(copy, reading: .groups(inView: viewID, layout: "board"))
tasks = LiveQuery(copy, reading: .list(inView: viewID, layout: "board"))
}
}
```

| Call | What it does |
| --- | --- |
| `items.views()`, `items.view(_:)` | Every view the copy holds, or one, `nil` when it isn't held. |
| `items.list(inView:layout:limit:offset:)` | A page of the view's items in a layout's sort. `layout: nil` is the first layout. |
| `items.groups(inView:layout:timeZone:)` | A layout's groups with their counts. Date groups follow `timeZone`, or the device's. |
| `search(_:inView:limit:)` | Full-text search within the view's query. |
| `Reading.views`, `.view(_:)`, `.list(inView:)`, `.groups(inView:layout:)` | The same reads, for a `LiveQuery`. |

**A view the copy can't answer whole is refused with its reason,** never answered in part: `invalid` names a type or tier the slice lacks, `notFound` with the code `not_held` means the copy doesn't hold the view, and `hydrationIncomplete` means a hydration is still filling the copy.

### Create, change and delete a view

`createView(_:idempotencyKey:)`, `changeView(_:_:baseVersion:idempotencyKey:)` and `deleteView(_:idempotencyKey:)` write at once and never queue, like the folder writes: with no server, or none reachable, they throw and nothing waits to be sent. A change replaces each setting it names whole and leaves the others as they are.

```swift
let attempt = UUID().uuidString
let made = try await copy.createView(
ViewSettings(
title: "Open tasks",
query: ViewQuery(types: ["core.task"], filter: "tags contains \"work\""),
layouts: [
ViewLayout(
id: "board", kind: .board, sort: [ViewSort(by: "created_at", direction: .descending)],
group: ViewGrouping(by: "properties.status", order: ["todo", "doing"])),
ViewLayout(id: "list", kind: .list),
]),
idempotencyKey: attempt)

let renamed = try await copy.changeView(
made.id, ViewSettingsChange(title: "Work tasks"), baseVersion: made.version)

try await copy.deleteView(renamed.id)
```

- **Retry a create with the same key.** If the connection drops after the request went out, the server may have made the view. Retrying with the same `idempotencyKey` answers the first view instead of making a second. Without a key, the SDK makes one for each call.
- **Name the version you changed from.** The server merges a change to settings nobody else touched since `baseVersion`. A setting changed since then throws `server` with status 409 and the code `version_conflict`, and the copy keeps the view it held. Read the view again and decide.
- **A delete removes the view for good.** The copy drops it at once, and a live query over the views stops listing it. A view the server no longer has throws `notFound` with the code `item_not_found`.

| Refusal | Thrown as |
| --- | --- |
| A query the server doesn't accept | `validation` with the code `validation_error` |
| A key that may not write `system.view` | `forbidden` with the code `type_not_permitted` |
| A change to a revoked view | `validation` with the code `invalid_transition` |
| A stale change to a setting that moved | `server`, status 409, the code `version_conflict` |
| A delete of a view that is gone | `notFound` with the code `item_not_found` |
| No server, or none reachable | `noServer`, or `network` |

The rules are in the contract's [views written through a copy](https://github.com/withmarfa/marfa/blob/main/conformance/spec/device.md#views-written-through-a-copy) section.

## Folders on a Mac

On macOS, `Folders` keeps a directory of files in step with a folder on the instance, and shares the Mac's list of folders with `marfa folders`. **Nothing is written into the directory or sent until the first sync is confirmed.** [Sync a folder](/guides/sync-a-folder) sets one up, with Swift examples.
Expand Down
25 changes: 16 additions & 9 deletions content/docs/guides/read-a-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ marfa device --db ./copy.sqlite views groups <view-id> --layout main --time-zone

## Read a view in an app

Use the core's view reads on the same handle as other working-copy reads. Rust exposes `list_views`, `view`, `list_in_view`, `search_in_view` and `view_groups`; the core's Node and Swift bindings expose the corresponding camel-case methods.
Use the core's view reads on the same handle as other working-copy reads. Rust exposes `list_views`, `view`, `list_in_view`, `search_in_view` and `view_groups`; the core's Node binding exposes the corresponding camel-case methods, and the [Swift SDK](/swift-sdk#saved-views) wraps them in its own types.

For example, with an open Rust `Core` handle, read a page and the layout's totals:

Expand All @@ -71,15 +71,22 @@ let page = core.list_in_view(&view_id, Some("main"), Some(50), None)?;
let groups = core.view_groups(&view_id, "main", None)?;
```

| Read | Answer |
| --- | --- |
| `list_views` | The views held in the copy. |
| `view` | One view's settings and version. |
| `list_in_view` | A page of matching items, ordered by the selected layout. |
| `search_in_view` | Search matches within the view's query. |
| `view_groups` | The selected layout's group keys, counts and sums. |
With a Swift `WorkingCopy`, the same two reads are:

**These are core APIs.** The separately released [Swift SDK](/swift-sdk) adopts core APIs on its own release schedule. Read the selected layout to choose the presentation, and pass the device's time zone when asking for date groups.
```swift
let page = try await copy.items.list(inView: viewID, layout: "main", limit: 50)
let groups = try await copy.items.groups(inView: viewID, layout: "main")
```

| Core read | Swift SDK | Answer |
| --- | --- | --- |
| `list_views` | `items.views()` | The views held in the copy. |
| `view` | `items.view(_:)` | One view's settings and version. |
| `list_in_view` | `items.list(inView:layout:limit:offset:)` | A page of matching items, ordered by the selected layout. |
| `search_in_view` | `search(_:inView:limit:)` | Search matches within the view's query. |
| `view_groups` | `items.groups(inView:layout:timeZone:)` | The selected layout's group keys, counts and sums. |

Read the selected layout to choose the presentation, and pass the device's time zone when asking for date groups. The Swift SDK also writes views and follows a layout with a live query: [Saved views](/swift-sdk#saved-views) shows both.

## Handle a refused view

Expand Down
Loading