Guided tour¶
Offline Tasks calls every part of the library's public API from a screen you can see, so this page is the API told as an app: each screen, what it shows, the calls behind it, and the page that explains each call. Every call sits in the samples' data layer, and the docs pages show those very lines. The Android and iOS samples are one product; where they differ, the table says so. After the screens, a ten-minute walk through the app shows the library at work, with your project's dashboard open beside it.
Two things hold on every screen. The client is built once, with every setting written out at its default, and the app's own connectivity signal (Configuration). Every failure a call can throw goes through one mapping, a sentence for each kind (Conflicts and failures).
Tasks¶
The list, open tasks first, with a search field and four chips above it.
| What it shows | The calls | On iOS | Explained on |
|---|---|---|---|
| The list, live, with a cloud mark on each row whose change waits | selectAsFlow, with CacheFirstThenNetwork, or NetworkFirstThenCache while the Sync tab's Prefer fresh data is on; pendingMutations("tasks") and its rowKeys |
selectAsSequence, with .cacheFirstThenNetwork or .networkFirstThenCache; pendingMutations(table: "tasks") |
Reactive reads, Optimistic overlay |
| Search, and the All / Open / Done / Recent chips, as filters the server applies | ilike on the title; eq and is on done; Recent as gte and lt on created_at in one and group |
ilike; neq and is on done; Recent as gte and lt, which Swift sends in one group by itself |
Reactive reads, One-shot reads |
| How many photos each task has | A live read of the photo rows with in on task_id |
in("task_id", values:) on the live read |
Reactive reads |
| Pull to refresh, and the caption under the title: "Updated just now", "From this device, 3 minutes ago", and how many changes wait | select(readStrategy = NetworkFirst(maxAge)) of the list shown, chip and search included, since each query is cached on its own, and the result's isFromCache, fetchedAt and hasPendingMutations |
select(readStrategy: .networkFirst(maxAge:)), and the OfflineResponse's isFromCache, fetchedAt and hasPendingMutations |
One-shot reads |
| Tick a task done | update with a JSON body |
update with a [String: OfflineJSON] body |
Writes and the offline queue |
| Swipe a task away, with Undo | Its photo files (online only); its photos still waiting to upload, on the device; then delete of its photo rows and of the task |
The same, with no Undo | Writes and the offline queue, Storage caching |
| Mark all done, in the menu | rpc("complete_all_tasks"), a database function, network only; then the list shown and the whole list are read again, since a function's changes reach the device only by a read |
rpc("complete_all_tasks"), from the Tasks tab's menu |
Coming from supabase-kt |
| An alert when the server refused a waiting change | rejectedMutations().first() at launch, then the flow |
The same | Conflicts and failures |
| The badges: open tasks on Tasks, waiting changes on Sync, red when one was refused | select(readStrategy = CacheFirst(maxAge)) and pendingMutationCount() at launch, then the live list and syncStatus |
select(readStrategy: .cacheFirst(maxAge:)) and pendingMutationCount() at launch, then the live list and syncStatus |
One-shot reads, Replaying the queue |
New task¶
| What it shows | The calls | On iOS | Explained on |
|---|---|---|---|
| Title, notes, and Needs a connection | insert(row, mutationStrategy) { select() }: QueueIfOffline, the default, or OnlineOnly with the switch on, whose stored row the app then puts in the cache with applyServerRecord, since an online-only write is never patched in |
insert(row, mutationStrategy:).select().single(): .queueIfOffline or .onlineOnly, then applyServerRecord of the stored row |
Writes and the offline queue |
| "Saved at 10:42", or "saved on this device" | MutationOutcome.Applied.result, the row the server stored, or Queued |
MutationOutcome.applied(result:), the row the server stored, or .queued(pendingMutation:) |
Writes and the offline queue |
| Add photo, several at once, with their thumbnails, online or offline | None at first: once the task is saved, each photo is filed in the app's own pending-photo directory, and the uploader takes it from there (see Task detail) | The same, with a PhotosPicker of up to ten photos; a task that needs a connection and is not saved files none |
Storage caching |
Task detail¶
| What it shows | The calls | On iOS | Explained on |
|---|---|---|---|
| The task, even one never opened before, offline | The list's own query, with the row picked out on the device | The same | FAQ |
| Title and notes, saved as you type | update with a typed value |
update with a typed Encodable patch that writes a cleared note as null |
Writes and the offline queue |
| Duplicate task, in the menu | upsert(row) { select() }, then isQueued and resultOrNull() |
upsert(copy), under an id made from the task's, so a second copy writes over the first; then isQueued |
Writes and the offline queue |
| Delete task, the bin beside the done button, offline too | As swiping a task away | The same, from the last row of the task's form | Writes and the offline queue |
| The photos; with Simulate offline on, "Showing photos saved on this device" | selectAsFlow of the photo list, FlowReadStrategy.CacheOnly while network access is off |
selectAsSequence of the photo list, .cacheOnly while Simulate offline is on |
Reactive reads |
| Add photo, online or offline: the photo shows at once from the device, marked "Waiting to upload", "Uploading" (with its progress) or "Upload failed" with Retry and Remove | The app's pending-photo directory merged with the photo rows; the uploader watches syncStatus.network, then, one photo at a time, exists (on a retry), supabase-kt's uploadAsFlow on the offline bucket, upsert of the photo's row with ignoreDuplicates, and ensureCachedFile |
In the task's Photos section, with no progress: the uploader follows syncStatus, then exists(path:) (on a retry), upload(_:data:options:) with UploadOptions (contentType, upsert, userMetadata), upsert(_:onConflict:ignoreDuplicates:) of the row and ensureCachedFile(path:) |
Storage caching |
| Share all photos of this task, in the menu | createSignedUrls |
createSignedURLs(paths:expiresIn:) |
Storage caching |
Photo viewer¶
| What it shows | The calls | On iOS | Explained on |
|---|---|---|---|
| The photo, offline too | ensureLocalUri; the thumbnails before it, localUriIfCached |
cachedFile, else ensureCachedFile, read from CachedFile.url |
Storage caching |
| Details: in the bucket or not, size, type, times, ETag, the public and authenticated URLs, and the copy on this device | exists, info, publicUrl, authenticatedUrl, cachedFile and its fetchedAt and sizeBytes |
exists, info, publicURL, authenticatedURL, cachedFile and its fetchedAt, sizeBytes and path |
Storage caching |
| Share | ensureLocalFile, copied out of the cache for the share sheet |
ensureCachedFile's CachedFile.url, handed to the share sheet |
Storage caching |
| Share link: a copy 512 pixels wide, for an hour | createSignedUrl with an image transformation |
createSignedURL(path:expiresIn:transform:) with an ImageTransformation |
Storage caching |
| Replace photo, Move to another task, Duplicate to another task, Delete photo | update, move with an update of the row, copy with an insert, delete; all online only |
update(_:data:options:), move(from:to:) with an update of the row, copy(from:to:) with an insert, remove(paths:) |
Storage caching |
| A photo waiting to upload: its own file, a line saying the bucket's actions wait for the upload, Share, and Remove (not while it uploads), with Retry once it failed, offline too | No library call: the file and its record are the app's | The same | Storage caching |
Photos¶
| What it shows | The calls | On iOS | Explained on |
|---|---|---|---|
| Every photo, newest first, a page at a time, each marked when it is on the device | selectAsFlow with order and range; Show more reads a longer page |
selectAsSequence with order and limit |
Reactive reads |
| The photos waiting to upload, among them with their marks | The app's pending-photo directory, merged with the page | The same | Storage caching |
Sync¶
| Section | What it shows | The calls | On iOS | Explained on |
|---|---|---|---|---|
| Connection | The network, and Simulate offline | syncStatus.network; disableNetwork() and enableNetwork() |
The same | Replaying the queue |
| Settings | Prefer fresh data, and Live updates with where its channel stands | The list's read strategy; supabase-kt's Realtime, each change applied with applyServerRecord or applyServerDelete, typed for tasks and as JSON for photos |
supabase-swift's Realtime (RealtimeClientV2), each change applied with applyServerRecord or applyServerDelete, typed for tasks and as [String: OfflineJSON] for photos |
Reactive reads, Applying Realtime events |
| Waiting changes | The count per table, one row per change ("Added “Buy milk”, 2 minutes ago") with Discard, when it last synced or why it stopped, and Sync now with every count of its report | pendingMutationCountFlow(table), pendingMutations(), discardPendingMutation(id), lastSyncedAt, lastSyncError, syncPendingMutations() and its SyncReport |
pendingMutationCountSequence(table:), pendingMutations(), discardPendingMutation(id:), then the same |
Writes and the offline queue, Replaying the queue |
| Refresh | Where the list's refresh stands | QuerySnapshot.refresh |
The same | Refresh lifecycle |
| Refused changes | Each refused change, when it was made and refused, its rows and the server's answer, with Retry (off when it cannot be retried) and Dismiss | rejectedMutations(), retryRejectedMutation(id), dismissRejectedMutation(id) |
The same, with what the change did, and the server's details and hint | Conflicts and failures |
| Server | "On the server: 7 of 12 done · 3 this week" | A count, and rpc("tasks_done_count", …) with typed and JSON parameters |
The same | One-shot reads, Coming from supabase-kt |
| Storage | Your folders in the bucket with their files, and Upload through a signed URL | list with a limit, an offset, a sort and a search; createSignedUploadUrl and uploadToSignedUrl |
Browse photo files: list(path:options:) with a StorageListFilter; the signed-URL upload is under Try it |
Storage caching |
| Try it | Queue an invalid title, Add five sample tasks, Import the starter list, Delete done tasks offline, Two filters on one column, Count open tasks on the server; each says what it called and what happened | insert of a JSON body, insert of a list, upsert with onConflict and ignoreDuplicates, a grouped delete refused offline with MutationNotQueueableException, a read refused with IllegalArgumentException, a count with filter(column, operator, value) |
Count tasks with notes (filter("notes", operator: "not.is", value: "null")), Delete last week's done tasks and Upload through a signed URL in place of the count, the grouped delete and the two filters, which Swift sends in one group |
Writes and the offline queue, One-shot reads |
| Local data | Clear local cache, and Start over | clearLocalData(), then the app's pending-photo directory, which the library does not touch; Start over clears, deletes the user's files and rows OnlineOnly, clears the pending photos, then signs in as a new user |
Clear local cache the same; Start over waits for a photo upload in progress, discards the user's waiting changes, deletes their files and rows .onlineOnly, turns the network off, signs out, clears the device and then the pending photos, signs in anew and turns the network back on |
Signing out and switching users |
Coming back to the app sends what waits: the app calls syncPendingMutations() as it comes to the
foreground (Replaying the queue).
Ten minutes with the app¶
Online basics¶
- Open the app. It signs in anonymously and shows an empty list.
- Add a task with New task: the app says it was saved, with the time the server stored it
(the row's
updated_at, which the server sets;created_atis the time the device made it). Open it, add some notes and tick it done. - In the dashboard, open the Table Editor and the
taskstable: the row is there, with your anonymous user's id inuser_idand the id the app made on the device inid.
Each change was sent at once, because nothing waited ahead of it.
Live updates¶
With Live updates on (the Sync tab's Settings), change a task's title in the Table Editor. The app shows the new title within a second or two: Realtime sent the row, and the app applied it to the list it keeps on the device.
Go offline¶
On the Sync tab, switch on Simulate offline. It turns the library's network access off, as airplane mode would, for this app only and until you switch it back or the app restarts. The banner and the subtitle say the app is offline, and Sync now is switched off.
Write while offline¶
Add a task, clear another's notes, tick one done and delete one. Each change shows at once, with a cloud mark on the rows whose change has not reached the server; the banner and the Sync tab's badge count the changes waiting, and the Sync tab lists each one. Pull to refresh: the caption under the title says the list is from this device, and how old it is.
Open a task you have never opened before: it opens offline too, because the app reads its list once and finds the task in it. Tap a chip or search for a word you never asked for online: there is nothing saved for it, and the app says so.
Come back online¶
Switch Simulate offline off. The library sends the waiting changes by itself, in the order you made them, and the count drops to Nothing waiting.
Check the dashboard¶
Refresh the tasks table in the Table Editor: the new task is there, the notes are cleared, the
task is done, and the deleted one is gone. Each row's updated_at is the server's time, set when
the change arrived.
Cold start offline¶
Simulate offline does not survive a restart, so use the device's airplane mode for this one.
- Turn on airplane mode and close the app completely.
- Open it again. The list is there, read from the device; the session is the same anonymous user, restored from the device too.
- Add a task, then close the app again. The change is kept on the device.
- Turn airplane mode off and open the app: the task is sent.
Photos¶
- Online, open a task and choose Add photo. The photo shows at once, marked while it uploads (on Android with its progress), and the mark goes once the upload is done.
- Open the photo, then Details: the file is in the bucket and on this device. Try Share link and Move to another task.
- Switch on Simulate offline, open New task, give it a title and Add photo (pick two), then save. Open the task: its photos show at once, from the device, each marked "Waiting to upload". The Photos tab has them too. Open one: the viewer shows it, and says why Details, Share link, Replace, Move and Duplicate need the photo on the server.
- In the dashboard, the
task_photostable and the bucket's folder for the task have nothing new: Storage writes are never queued, so the photos wait in the app's own queue. - Switch Simulate offline off. The queued task is sent and the photos upload one at a time;
each photo's row follows once Storage has its file, behind its task's, and its mark goes.
Refresh
task_photosand the bucket: both photos are there. - Every other change to a photo (Replace, Move, Duplicate, Delete) still needs the network: offline they are switched off, and the photos already on the device still show.
A refused write¶
On the Sync tab, under Try it, choose Queue an invalid title while offline. It adds a task with a blank title, which the schema refuses, and the change waits like any other. Come back online: the server refuses it (HTTP 400, Postgres code 23514), and it moves to Refused changes with the server's answer and the Tasks tab says so. Retry sends it again; Dismiss removes it.
Online, the same item's write is refused as soon as it is sent, and the line under it says why: a write sent at once that the server refuses is thrown to the app, not listed.
Writes the queue cannot hold¶
Still under Try it, with Simulate offline on, choose Delete done tasks offline. A delete filtered by a group of conditions cannot wait in the queue, so the library refuses it at once and says which kind of write it was; nothing is queued. Two filters on one column is refused online and offline alike, before anything is sent. On iOS the item is Delete last week's done tasks, refused offline in the same way, and there is no Two filters on one column: the Swift package sends both conditions in one group by itself.
Try a conflict¶
Two devices are two anonymous users, so play the second writer from the Table Editor. Switch Live updates off first, so the app shows only what it reads.
- Go offline in the app and tick a task done.
- In the Table Editor, change the same task's title.
- Come back online and pull to refresh. Both changes are there: the app's queued update sets
only
done, so it leaves the title the dashboard wrote. - Now go offline, change a task's title in the app, change the same title in the Table Editor, and come back online. The app's title wins: the last write to reach the server wins, and the app's arrived last.
The library does not merge or resolve conflicts; see Conflicts and failures.
Start again¶
Under Local data on the Sync tab:
- Clear local cache deletes what the device holds, the queue and the photos waiting to upload included, then reads both lists again from your project.
- Start over deletes this user's photos, photo rows and tasks from your project, clears the device and the photos waiting to upload, and signs in as a new anonymous user. It needs the network.