Sift
One object, three piles, four screens.
A screenshot is the only thing the app is about. It ends up in the trash list, the archive list or Photos’ favorites; the queue is whatever is in none of them and at least 30 days old, newest first, and a newer one waits until it comes of age. Every screen, route and side effect on this page follows from those two rules. The written version is docs/knowledge/IA.md.
Object model
One object the app is about, three memberships it can hold, and a queue that is computed rather than stored. A verdict is not a field on the screenshot; it is which list the screenshot is in.
Derivation rules
1 · unreviewed, and due
Not in the trash list, not in the archive list, not favorited. The queue is every unreviewed screenshot taken at least 30 days before the last refresh, newest first; the front card is the newest of them. A newer unreviewed one is waiting: on no screen until it comes of age, then in the queue by itself (ADR-032).
2 · Trash wins on display
A screenshot in the trash list appears only in Trash, even if it is also favorited or archived. The app clears the other two when it trashes; the rule covers edits made in Photos afterwards.
3 · Library segments
Favorites is favorited and not trashed; Archive is in the archive list and not trashed. A screenshot can be in both segments and then appears in both.
4 · Counter
total is what the app can see; reviewTotal is the due ones among them, reviewed or not; reviewed is reviewTotal minus the queue. The header shows reviewed + 1 / reviewTotal while a card is up; when the queue is empty the all-done block takes its place. A waiting screenshot counts in total only.
Screen inventory
Four primary screens, three subordinate surfaces, six system surfaces. Nothing else exists in v1. Regions, components and strings for each are in UI_DESIGN section 11.
The thirteen surfaces
| # | Surface | Kind | Reached from | Leaves to | Blocks · empty |
|---|---|---|---|---|---|
| P | Permission | gate · replaces the root while access is unusable | launch when status is not usable; Review when authorization is revoked | Review, on grant | onboarding · denied · limited interstitial |
| R | Review | root of the stack | Permission on grant; launch when access is usable | Trash and Library, by push | allDone · noScreenshots |
| T | Trash | pushed | Review header icon; the allDone CTA | back · Viewer · Purge-all sheet | trashEmpty |
| L | Library | pushed | Review header icon | back · Viewer · Credits | favoritesEmpty · archiveEmpty |
| V | Viewer | full-screen cover, black | a cell in Trash or Library | dismiss | two action sets, chosen by the presenting screen |
| S | Purge-all sheet | sheet over Trash | the docked destructive button | Keep them · Delete N → the iOS delete dialog | — |
| C | Credits | pushed | Library footer; the Permission footer before access is granted | back | — |
| X1 | iOS photo-library dialog | system | Permission CTA | Review · limited interstitial · denied | — |
| X2 | Limited-library picker | system | Pick more | the interstitial, with the new count | — |
| X3 | iOS delete dialog | system | Viewer · Delete permanently; the Purge-all sheet | Trash, cells removed, or the “Not deleted” toast | — |
| X4 | Settings app | external | Open Settings; Turn on reminders in Settings on the allDone block | re-routed on the next scenePhase active | — |
| X5 | iOS notification dialog | system | Remind me every 30 days on the allDone block, the first time only | the allDone block, with the reminder on or declined | — |
| X6 | The reminder | system notification: a banner, then Notification Center | iOS, 30 days after the queue last ran out under review | a tap opens the app, routed as at launch | — |
Launch routing
Evaluated at launch and again on every return to the foreground, so revocation, a return from Settings and a change to the limited selection all land in the right place without a tap.
As a tree
active ├── notDetermined → Permission · onboarding ├── denied | restricted → Permission · denied ├── limited │ ├── selected ≠ acknowledged → Permission · limited interstitial │ └── otherwise → Review └── authorized → Review Review ├── total == 0 → noScreenshots block ├── queue empty → allDone block, Trash count and the next reminder or else the │ next arrival in the copy; also when every screenshot is still waiting └── otherwise → the deck
Queue lifecycle
The states of the Review screen and every event that moves between them. Rewind holds exactly one entry (ADR-008): replaced on every commit, cleared on cold launch and at the cap, invalidated when its screenshot changes hands elsewhere.
Transitions
| From | Event | To | Side effect |
|---|---|---|---|
reviewing | drag starts | dragging | — |
dragging | release below threshold, or the down sector | reviewing | snap back |
dragging | release past DSSwipe.commitDistance, or commitVelocity with minTravelForVelocity | exiting | throw |
reviewing | verdict button · arrow key · VoiceOver action | exiting | synthesized flick |
exiting | 60 % of the exit duration | promoting | the verdict's side effect; rewind entry replaced |
promoting | queue non-empty | reviewing | next card fades in |
promoting | queue empty | allDone | DSHaptic.queueDone; the reminder, if on, moved to 30 days from now |
reviewing · allDone | rewind, entry present | rewinding | side effect reversed exactly |
rewinding | landing | reviewing | — |
allDone | a due screenshot becomes unreviewed again, or a waiting one comes due at a refresh | reviewing | it takes its place by date |
| any | authorization revoked | Permission · denied | stack discarded |
reviewing · allDone | a new screenshot under 30 days old appears | same state; total + 1, the counter unchanged | it waits |
noScreenshots | a new screenshot under 30 days old appears | allDone | it waits; the block names the day it comes due; no burst |
reviewing | a new screenshot already 30 days old appears | same state; total and reviewTotal + 1 | joins by date, behind the front card, never under the thumb |
allDone · noScreenshots | a new screenshot already 30 days old appears | reviewing | total and reviewTotal + 1; it takes its place by date |
reviewing | a waiting screenshot comes due at a refresh | same state, reviewTotal + 1 | joins behind the front card, never under the thumb |
reviewing | the front screenshot is deleted in Photos | promoting | skipped silently, no verdict |
The reminder
| Reminder | Meaning | The allDone block shows |
|---|---|---|
| off | notifications allowed, or never asked for, and nothing pending | “Remind me every 30 days”, and the “Next screenshot” line when one waits |
| on | notifications allowed and the request pending | “Next reminder: Nov 28.” in place of the “Next screenshot” line, and no reminder action |
| denied | notifications declined, or allowed with alerts, the lock screen and Notification Center all off | “Turn on reminders in Settings”, and the “Next screenshot” line when one waits |
DSHaptic.queueDone, the reminder, if it is on, is replaced by one that fires 30 days from that moment and every 30 days after. A launch that starts out done and a first screenshot that arrives and waits finish nothing and leave it where it is. The three states above are read from iOS when the allDone block appears and on every return to the foreground.Actions and where they lead
Every action the user can take, and what it does to the three memberships. A Photos write happens on FAVE, on ARCHIVE and on permanent deletion; only deletion presents a system dialog.
| Where | Action | Trash list | Archive list · album | Favorite | Queue | Confirmation |
|---|---|---|---|---|---|---|
| Review | swipe left · Trash button | add | — | — | leaves | none |
| Review | swipe right · Archive button | — | add · add to album | — | leaves | none |
| Review | swipe up · Fave button | — | — | set true | leaves | none |
| Review | rewind | exact undo, using the recorded prior favorite flag | returns to the front | none | ||
| Trash | Restore | remove | — | — | returns, by date | none |
| Trash | Delete permanently, one | remove after success | — | — | — | the iOS dialog |
| Trash | Delete all permanently | remove all after success | — | — | — | the sheet, then the iOS dialog |
| Library · Favorites | Move to Archive | — | add · add to album | set false | — | none |
| Library · Archive | Move to Favorites | — | remove · remove from album | set true | — | none |
| Library · Viewer | Trash | add | remove · remove from album | set false | — | none |
| Permission | Show me the screenshots | — | — | — | — | the iOS photo-library dialog |
| Permission · limited | Pick more · Sift these N (the ready ones), or Continue when none are ready | — | — | — | re-evaluated | the picker · none |
| Permission · denied | Open Settings | — | — | — | — | leaves the app |
| Review · all done | Remind me every 30 days | — | — | — | — | the iOS notification dialog, the first time only |
| Review · all done | Turn on reminders in Settings | — | — | — | — | leaves the app |
Persistence
Two lists and two scalars persist across launches; one entry lives in memory; the favorite flag lives in Photos. Nothing else is stored, on purpose.
| Data | Lives in | Keyed by | Survives reinstall | Cleared by |
|---|---|---|---|---|
| Trash list | app-local store | localIdentifier, trashedAt | no | restore · purge |
| Archive list | app-local store | localIdentifier, archivedAt | no, but recoverable from the album | move · trash |
| Archive album identifier | app-local store | the album's localIdentifier | no, re-found by title | — |
| Favorite | Photos | — | yes | the user, in either app |
| Acknowledged selection count | app-local store | — | no | access becomes authorized |
| Rewind entry | memory | — | no | cold launch · cap · invalidation |
| Authorization | the system | — | yes | the user, in Settings |
| Reminder | iOS, as the pending notification request | sift.reminder, with the moment it was added | no: uninstalling removes it and the notification permission | nothing in the app; notifications off in Settings silence it without removing it |
Deliberately not stored
No “onboarding seen” flag: Permission appears whenever the status makes it necessary and never otherwise. No per-screenshot “reviewed” flag: derived. No “waiting” flag and no schedule for when a screenshot comes due: derived from its creation date. No copy of the favorite flag. No session log, no statistics. No reminder flag and no reminder date: on is notification authorization plus the pending request, and its next date is computed from that request (ADR-034).
After a reinstall
The trash list is gone, and because nothing was deleted from Photos the trashed screenshots simply return to the queue (a recent one waits until it is 30 days old). The archive list is gone too, but the album is not: an empty store adopts an album titled with the brand name and seeds the list from its members.
External change
The app observes the library and reconciles on every change and on every return to the foreground. Because the queue is derived, most edits made in Photos need no special case; these are the ones with a visible effect.
| Change made in Photos | Effect in the app |
|---|---|
| A screenshot is deleted | dropped from the queue and both lists silently; if it was the front card the deck promotes without a verdict |
| A screenshot is taken | waits 30 days: total rises at once, the counter does not; noScreenshots becomes allDone, whose copy names the day it comes due. At the first refresh after that it joins the queue by date, behind the front card |
| A screenshot is hearted | leaves the queue; appears in Library · Favorites |
| A heart is removed | returns to the queue unless it is in the trash or archive list |
| A screenshot is removed from the archive album by hand | treated as un-archiving: the entry is dropped, the screenshot returns to the queue |
| The archive album is deleted | the list is the truth: the album is recreated and repopulated on the next archive or launch |
| The limited selection changes | the interstitial shows on the next return to the foreground |
| Authorization is revoked | Permission · denied; the stack is discarded |
| Access widens from limited to full | Review; the acknowledged selection count is cleared |
System surfaces
Surfaces the app presents but does not design. Their copy and styling are iOS’s; the app decides only when to present them and what to do with each outcome.
| Surface | Presented by | Outcomes the app handles |
|---|---|---|
| Photo-library access dialog | requestAuthorization(for: .readWrite) on the Permission CTA, never on launch | authorized → Review · limited → interstitial · denied → denied state |
| Limited-library picker | presentLimitedLibraryPicker(from:) on Pick more | on dismiss the selection count is re-read and the interstitial updates |
| Delete confirmation | performChanges with deleteAssets — one dialog per call, so “all” is one batch | confirmed → cells removed, purgeDone · cancelled → “Not deleted”, nothing changes · partial failure → “Couldn't delete N. Try again.” |
| Settings | UIApplication.openSettingsURLString on Open Settings; openNotificationSettingsURLString, the app’s notification settings, on Turn on reminders in Settings | re-routed on the next scenePhase active, when the allDone block also reads the reminder again |
| Notification permission dialog | requestAuthorization(options: [.alert, .sound]) on Remind me every 30 days, never on launch, and only while iOS has never asked | allowed → the reminder is scheduled, permissionGranted, the block reads “Next reminder”, VoiceOver hears the date · declined → “Turn on reminders in Settings”, and VoiceOver hears it |
| The reminder | one request, sift.reminder, with a repeating 30-day time-interval trigger: added on Remind me every 30 days, replaced each time the queue runs out under review. Its title and body are the app’s words (UI_DESIGN section 12) | delivered as a banner and to Notification Center, not shown while the app is open · a tap opens the app, routed as at launch · off, on or denied |
Assumptions, confirmed
Structural choices this architecture had to make that no earlier ADR settled. The owner confirmed all four on 2026-09-27; they are recorded as ADR-025, status Active. Each is a rule above; the alternative it displaced is kept for the record.
A pre-existing heart counts as reviewed
A screenshot hearted in Photos before the app ever saw it is a Favorite and never enters the queue. The alternative is an app-side “seen” set, which makes FAVE a no-op on an already-hearted card.
The limited interstitial shows only when the selection changed
On first grant, and whenever the selected count differs from the one last acknowledged; otherwise limited access goes straight to Review. The alternative is showing it on every launch.
Credits lives in the Library footer
Once access is granted the Permission footer is gone, so the Library footer carries the link. The alternatives are a header overflow menu on Review, or the Settings bundle.
The archive list is the truth; the album mirrors it
A deleted album is recreated, a hand-removed member is un-archived, an empty store adopts an existing album. The alternative, the album as truth, loses every archive verdict if the user deletes the album.
Out of scope for v1
Named so nobody has to wonder.
Search and text filters · a Settings screen (so the reminder is turned off in iOS Settings, ADR-034) · statistics and streaks · multi-select in Trash (ADR-015) · deep links, widgets and Shortcuts · an iPad layout · syncing the app-local lists through iCloud · reviewing photos that are not screenshots · any AI assistance.