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.

Confirmed ADR-025 ADR-032 ADR-033 ADR-034 v1.3
01

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.

Screenshot PHAsset · Screenshots smart album identity: localIdentifier Trash list app-local · the asset stays in Photos Archive list app-local · mirrored to the album Favorite Photos · PHAsset.isFavorite swipe left · TRASH swipe right · ARCHIVE swipe up · FAVE Queue none of the three · 30+ days old newest first · derived, never stored not in any of the three

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.

Consequence. A screenshot hearted in Photos before the app ever saw it is already a Favorite and never enters the queue; un-hearting it later puts it back. The app owns no “reviewed” flag that could drift from what Photos shows, and no “waiting” flag either: being due is the creation date against the last refresh. That is assumption A1 below.
02

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.

Permission gate · onboarding · limited · denied iOS photo-library dialog Limited-library picker Settings app re-routed on the next scenePhase == .active CTA Pick more Open Settings Review root · the deck · counter granted revoked iOS notification dialog The reminder every 30 days · a tap opens the app Remind me Turn on reminders Trash pushed · the holding pen Library pushed · Favorites · Archive push · header icon · Open Trash push · header icon Viewer Restore · Delete permanently Purge-all sheet Keep them · Delete N iOS delete dialog Viewer Move · Trash Credits pushed · footer tap cell docked button Delete N Delete permanently, one tap cell footer link
gaterootpushedfull-screen coversheetsystem or external, not ours to design

The thirteen surfaces

#SurfaceKindReached fromLeaves toBlocks · empty
PPermissiongate · replaces the root while access is unusablelaunch when status is not usable; Review when authorization is revokedReview, on grantonboarding · denied · limited interstitial
RReviewroot of the stackPermission on grant; launch when access is usableTrash and Library, by pushallDone · noScreenshots
TTrashpushedReview header icon; the allDone CTAback · Viewer · Purge-all sheettrashEmpty
LLibrarypushedReview header iconback · Viewer · CreditsfavoritesEmpty · archiveEmpty
VViewerfull-screen cover, blacka cell in Trash or Librarydismisstwo action sets, chosen by the presenting screen
SPurge-all sheetsheet over Trashthe docked destructive buttonKeep them · Delete N → the iOS delete dialog—
CCreditspushedLibrary footer; the Permission footer before access is grantedback—
X1iOS photo-library dialogsystemPermission CTAReview · limited interstitial · denied—
X2Limited-library pickersystemPick morethe interstitial, with the new count—
X3iOS delete dialogsystemViewer · Delete permanently; the Purge-all sheetTrash, cells removed, or the “Not deleted” toast—
X4Settings appexternalOpen Settings; Turn on reminders in Settings on the allDone blockre-routed on the next scenePhase active—
X5iOS notification dialogsystemRemind me every 30 days on the allDone block, the first time onlythe allDone block, with the reminder on or declined—
X6The remindersystem notification: a banner, then Notification CenteriOS, 30 days after the queue last ran out under reviewa tap opens the app, routed as at launch—
04

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.

App becomes active launch · foreground notDetermined denied · restricted limited authorized Permission · onboarding Permission · denied Permission · limited Review count changed otherwise noScreenshots allDone the deck total 0 queue empty otherwise

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
A tap on the reminder is a launch or a return to the foreground like any other: this table decides where it lands, and the reminder carries no route of its own (ADR-034).
05

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.

loading reviewing dragging rewinding promoting exiting noScreenshots allDone Permission · denied fetched total 0 due screenshot new screenshot, waits rewind · landed drag release short · snap back commit button · key · VoiceOver 60 % of the exit next card queue empty back to reviewing when a screenshot becomes unreviewed again: restore · move out of Library · un-heart in Photos · a screenshot coming due authorization revoked, from any state

Transitions

FromEventToSide effect
reviewingdrag startsdragging—
draggingrelease below threshold, or the down sectorreviewingsnap back
draggingrelease past DSSwipe.commitDistance, or commitVelocity with minTravelForVelocityexitingthrow
reviewingverdict button · arrow key · VoiceOver actionexitingsynthesized flick
exiting60 % of the exit durationpromotingthe verdict's side effect; rewind entry replaced
promotingqueue non-emptyreviewingnext card fades in
promotingqueue emptyallDoneDSHaptic.queueDone; the reminder, if on, moved to 30 days from now
reviewing · allDonerewind, entry presentrewindingside effect reversed exactly
rewindinglandingreviewing—
allDonea due screenshot becomes unreviewed again, or a waiting one comes due at a refreshreviewingit takes its place by date
anyauthorization revokedPermission · deniedstack discarded
reviewing · allDonea new screenshot under 30 days old appearssame state; total + 1, the counter unchangedit waits
noScreenshotsa new screenshot under 30 days old appearsallDoneit waits; the block names the day it comes due; no burst
reviewinga new screenshot already 30 days old appearssame state; total and reviewTotal + 1joins by date, behind the front card, never under the thumb
allDone · noScreenshotsa new screenshot already 30 days old appearsreviewingtotal and reviewTotal + 1; it takes its place by date
reviewinga waiting screenshot comes due at a refreshsame state, reviewTotal + 1joins behind the front card, never under the thumb
reviewingthe front screenshot is deleted in Photospromotingskipped silently, no verdict

The reminder

ReminderMeaningThe allDone block shows
offnotifications allowed, or never asked for, and nothing pending“Remind me every 30 days”, and the “Next screenshot” line when one waits
onnotifications allowed and the request pending“Next reminder: Nov 28.” in place of the “Next screenshot” line, and no reminder action
deniednotifications 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
A finished queue moves the reminder (ADR-034). Each time the queue runs out under review, the transition that fires 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.
06

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.

WhereActionTrash listArchive list · albumFavoriteQueueConfirmation
Reviewswipe left · Trash buttonadd——leavesnone
Reviewswipe right · Archive button—add · add to album—leavesnone
Reviewswipe up · Fave button——set trueleavesnone
Reviewrewindexact undo, using the recorded prior favorite flagreturns to the frontnone
TrashRestoreremove——returns, by datenone
TrashDelete permanently, oneremove after success———the iOS dialog
TrashDelete all permanentlyremove all after success———the sheet, then the iOS dialog
Library · FavoritesMove to Archive—add · add to albumset false—none
Library · ArchiveMove to Favorites—remove · remove from albumset true—none
Library · ViewerTrashaddremove · remove from albumset false—none
PermissionShow me the screenshots————the iOS photo-library dialog
Permission · limitedPick more · Sift these N (the ready ones), or Continue when none are ready———re-evaluatedthe picker · none
Permission · deniedOpen Settings————leaves the app
Review · all doneRemind me every 30 days————the iOS notification dialog, the first time only
Review · all doneTurn on reminders in Settings————leaves the app
Under limited access, if PhotoKit refuses the album write, the archive list still records the verdict and the album catches up when access widens (ADR-010).
The two reminder actions change no membership. Remind me every 30 days adds the one pending notification request, asking for permission first if iOS never has; after that it is replaced each time the queue runs out under review (section 05, ADR-034).
07

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.

DataLives inKeyed bySurvives reinstallCleared by
Trash listapp-local storelocalIdentifier, trashedAtnorestore · purge
Archive listapp-local storelocalIdentifier, archivedAtno, but recoverable from the albummove · trash
Archive album identifierapp-local storethe album's localIdentifierno, re-found by title—
FavoritePhotos—yesthe user, in either app
Acknowledged selection countapp-local store—noaccess becomes authorized
Rewind entrymemory—nocold launch · cap · invalidation
Authorizationthe system—yesthe user, in Settings
ReminderiOS, as the pending notification requestsift.reminder, with the moment it was addedno: uninstalling removes it and the notification permissionnothing 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.

08

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 PhotosEffect in the app
A screenshot is deleteddropped from the queue and both lists silently; if it was the front card the deck promotes without a verdict
A screenshot is takenwaits 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 heartedleaves the queue; appears in Library · Favorites
A heart is removedreturns to the queue unless it is in the trash or archive list
A screenshot is removed from the archive album by handtreated as un-archiving: the entry is dropped, the screenshot returns to the queue
The archive album is deletedthe list is the truth: the album is recreated and repopulated on the next archive or launch
The limited selection changesthe interstitial shows on the next return to the foreground
Authorization is revokedPermission · denied; the stack is discarded
Access widens from limited to fullReview; the acknowledged selection count is cleared
Returns to the queue means becomes unreviewed: a screenshot under 30 days old waits instead, and joins at the first refresh after it comes of age (ADR-032).
Never under the thumb. A screenshot that arrives, leaves, changes membership or comes due mid-drag is applied behind the pinned front card; the front card itself changes only at the next promoting.
09

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.

SurfacePresented byOutcomes the app handles
Photo-library access dialogrequestAuthorization(for: .readWrite) on the Permission CTA, never on launchauthorized → Review · limited → interstitial · denied → denied state
Limited-library pickerpresentLimitedLibraryPicker(from:) on Pick moreon dismiss the selection count is re-read and the interstitial updates
Delete confirmationperformChanges with deleteAssets — one dialog per call, so “all” is one batchconfirmed → cells removed, purgeDone · cancelled → “Not deleted”, nothing changes · partial failure → “Couldn't delete N. Try again.”
SettingsUIApplication.openSettingsURLString on Open Settings; openNotificationSettingsURLString, the app’s notification settings, on Turn on reminders in Settingsre-routed on the next scenePhase active, when the allDone block also reads the reminder again
Notification permission dialogrequestAuthorization(options: [.alert, .sound]) on Remind me every 30 days, never on launch, and only while iOS has never askedallowed → 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 reminderone 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
10

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.

A1

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.

A2

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.

A3

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.

A4

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.

Open, not assumed. Whether the Review card offers pinch-to-zoom. The design system gives zoom to the Viewer only; the card is the screenshot at card size.
11

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.