docs(apple): drop [FIXED] tags from the plan
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped

Decisions stay recorded; the tag implied false permanence. Open items
keep their [OPEN / TRACKED] / [GATE] markers.
This commit is contained in:
Ilia Denisov
2026-07-15 19:24:45 +02:00
parent ebc413f5e5
commit e04e97fb66
+24 -29
View File
@@ -4,8 +4,8 @@ Native **SwiftUI** application for iPhone and iPad, written **from scratch** (no
wrapper, unlike the Android app under `ui/android`). This document is the **source of truth and wrapper, unlike the Android app under `ui/android`). This document is the **source of truth and
continuity** for the effort: it lives in the repo so it travels to whatever machine does the work — continuity** for the effort: it lives in the repo so it travels to whatever machine does the work —
in particular the **Mac Claude session** that does the actual building. Read it first; append to it in particular the **Mac Claude session** that does the actual building. Read it first; append to it
as decisions land. Nothing here is final until built and verified; where a decision is locked it is as decisions land. Nothing here is final until built and verified; any decision can be reopened.
marked **[FIXED]**. Items still open carry an **[OPEN / TRACKED]** or **[GATE]** marker.
Chat with the owner is in Russian per the user-level guide; this doc, like all repo docs, is in Chat with the owner is in Russian per the user-level guide; this doc, like all repo docs, is in
**English**. **English**.
@@ -62,8 +62,7 @@ simulator installed generally; the steps below are mostly **verify**, a few are
--- ---
## 2. Fixed decisions [FIXED] ## 2. Fixed decisions
| Topic | Decision | | Topic | Decision |
|---|---| |---|---|
| Language / UI | Native **SwiftUI**, from scratch; **not** Capacitor. | | Language / UI | Native **SwiftUI**, from scratch; **not** Capacitor. |
@@ -125,8 +124,7 @@ custom build phases, so custom needs are expressible; worst case escape hatches
--- ---
## 5. Determining optimization principles [FIXED] ## 5. Determining optimization principles
These five shape the architecture from the start (not "add later"): These five shape the architecture from the start (not "add later"):
1. **Push (APNs) instead of a live socket while backgrounded.** For a turn-based game we do **not** hold 1. **Push (APNs) instead of a live socket while backgrounded.** For a turn-based game we do **not** hold
@@ -159,8 +157,7 @@ These five shape the architecture from the start (not "add later"):
--- ---
## 7. Storage — persist eagerly, do not trust `willTerminate` [FIXED] ## 7. Storage — persist eagerly, do not trust `willTerminate`
There is **no reliable destructive callback** on iOS: `willTerminate` is often **not** called (swipe There is **no reliable destructive callback** on iOS: `willTerminate` is often **not** called (swipe
force-kill, memory kill happen silently). Relying on catching a "destroy" event is a trap. force-kill, memory kill happen silently). Relying on catching a "destroy" event is a trap.
@@ -191,8 +188,7 @@ there — they need the **codec unit tests**.
--- ---
## 9. Crash reporting & analytics [FIXED] ## 9. Crash reporting & analytics
- **Crashes: MetricKit.** `MXMetricManager` delivers crash/hang/CPU/disk diagnostics to the app; we - **Crashes: MetricKit.** `MXMetricManager` delivers crash/hang/CPU/disk diagnostics to the app; we
**upload the payload to our own backend** and view it in the existing **Grafana/logs** contour — fully **upload the payload to our own backend** and view it in the existing **Grafana/logs** contour — fully
self-hosted, zero third-party, no paid service. Apple's Xcode Organizer / App Store Connect also provides self-hosted, zero third-party, no paid service. Apple's Xcode Organizer / App Store Connect also provides
@@ -354,8 +350,7 @@ with the game screen (the vertical slice).
· `ShareLink` / share sheet (export) · **TipKit** (coachmarks) · grouped `List` (settings/legal) · SIWA · `ShareLink` / share sheet (export) · **TipKit** (coachmarks) · grouped `List` (settings/legal) · SIWA
native sheet (auth). native sheet (auth).
### Assets, tiles & icons [FIXED] ### Assets, tiles & icons- **Tiles are drawn as runtime vectors** (a rounded rect + a font glyph + the point value, on the board
- **Tiles are drawn as runtime vectors** (a rounded rect + a font glyph + the point value, on the board
`Canvas`) — **no image assets, no pre-generation, no local-store cache.** Resolution-independent (crisp `Canvas`) — **no image assets, no pre-generation, no local-store cache.** Resolution-independent (crisp
under zoom-on-drop), theme/variant/locale-adaptive for free. Cache rendered tile images under zoom-on-drop), theme/variant/locale-adaptive for free. Cache rendered tile images
(`ImageRenderer`) **only if** profiling later shows a need. The crossword loading splash **reuses the (`ImageRenderer`) **only if** profiling later shows a need. The crossword loading splash **reuses the
@@ -374,14 +369,14 @@ native sheet (auth).
#### F. Game screen — detail #### F. Game screen — detail
**Layout [FIXED]** **Layout**
- **iPhone: full board always visible + zoom-on-drop** (hybrid, web-parity, setting-gated by the - **iPhone: full board always visible + zoom-on-drop** (hybrid, web-parity, setting-gated by the
existing "Zoom the board" toggle). Board scaled to width; on tile drop it magnifies toward the drop existing "Zoom the board" toggle). Board scaled to width; on tile drop it magnifies toward the drop
point for precision. point for precision.
- **Bottom-anchored stack**, top→bottom: *(top strip — the ad banner (L) + connection chrome, else - **Bottom-anchored stack**, top→bottom: *(top strip — the ad banner (L) + connection chrome, else
empty)* → **seats header****board****rack****bottom toolbar**. Everything is packed to the empty)* → **seats header****board****rack****bottom toolbar**. Everything is packed to the
bottom; the top strip hosts the L banner / Connecting…/Offline/connection-lost chrome, else empty. bottom; the top strip hosts the L banner / Connecting…/Offline/connection-lost chrome, else empty.
- **Orientation [FIXED]: iPhone portrait-only; iPad portrait + landscape.** - **Orientation: iPhone portrait-only; iPad portrait + landscape.**
**Four regions** **Four regions**
1. **Scoreboard / header** — seats (name, score, whose-turn), unread dot (red msg / amber nudge), 1. **Scoreboard / header** — seats (name, score, whose-turn), unread dot (red msg / amber nudge),
@@ -426,7 +421,7 @@ native sheet (auth).
resign/add-friend/block/chat hidden; a started move stays as a draft); if already in chat/dictionary resign/add-friend/block/chat hidden; a started move stays as a draft); if already in chat/dictionary
you stay (chat read-only, dictionary keeps checking on-device). you stay (chat read-only, dictionary keeps checking on-device).
**UX decisions [FIXED]** **UX decisions**
- **Comms sheet:** a **single detented sheet** with a segmented control **[History | Chat | - **Comms sheet:** a **single detented sheet** with a segmented control **[History | Chat |
Dictionary]**; the board + seats header stay visible above; native swipe-to-dismiss + grabber. Dictionary]**; the board + seats header stay visible above; native swipe-to-dismiss + grabber.
**Opened by tapping the seats header** (where the unread dot / 💬 live). iPad landscape later adapts **Opened by tapping the seats header** (where the unread dot / 💬 live). iPad landscape later adapts
@@ -464,7 +459,7 @@ emerge while implementing the vertical slice, step 3.)*
#### D. Lobby — detail #### D. Lobby — detail
**UX decisions [FIXED]** **UX decisions**
- **List style:** native **inset-grouped `List`** (the Settings-app look — floating rounded section - **List style:** native **inset-grouped `List`** (the Settings-app look — floating rounded section
cards over a grouped background). Sections: **your turn / opponent's turn / finished** (empty hidden). cards over a grouped background). Sections: **your turn / opponent's turn / finished** (empty hidden).
- **Grouping (option A — unified lobby):** one lobby, all games in the three state sections. **Local - **Grouping (option A — unified lobby):** one lobby, all games in the three state sections. **Local
@@ -499,7 +494,7 @@ emerge while implementing the vertical slice, step 3.)*
#### E. New Game — detail #### E. New Game — detail
**UX decisions [FIXED]** **UX decisions**
- **Structure:** a top **segmented `[Quick game | With friends]`** + a contextual **inset-grouped form** - **Structure:** a top **segmented `[Quick game | With friends]`** + a contextual **inset-grouped form**
below (consistent with the lobby aesthetic). below (consistent with the lobby aesthetic).
- *Quick game:* opponent segmented **`[AI | Random]`** → variant (emblem chips) → RU "multiple words" - *Quick game:* opponent segmented **`[AI | Random]`** → variant (emblem chips) → RU "multiple words"
@@ -525,7 +520,7 @@ emerge while implementing the vertical slice, step 3.)*
multiple-words · **move timeout** (5 min24 h, default 24 h) · **hints** (allowed? how many each) · multiple-words · **move timeout** (5 min24 h, default 24 h) · **hints** (allowed? how many each) ·
**leaver's-tile disposition** (returned to bag / removed — 34-player games only). **leaver's-tile disposition** (returned to bag / removed — 34-player games only).
**UX decisions [FIXED] (sub-details)** **UX decisions (sub-details)**
- **Cap 🔒 prompt:** a 🔒 badge on the **Start** control (looks unavailable); a tap opens a native - **Cap 🔒 prompt:** a 🔒 badge on the **Start** control (looks unavailable); a tap opens a native
alert — guest: a "Sign in / Create account" action; signed-in: an informational "finish a current alert — guest: a "Sign in / Create account" action; signed-in: an informational "finish a current
game first." game first."
@@ -540,7 +535,7 @@ emerge while implementing the vertical slice, step 3.)*
#### C. Login / identity — detail (v1: SIWA + email + guest) #### C. Login / identity — detail (v1: SIWA + email + guest)
**UX decisions [FIXED]** **UX decisions**
- **No hard login wall — guest by default** (offline-first, as Android native). First launch drops - **No hard login wall — guest by default** (offline-first, as Android native). First launch drops
straight into the lobby as a guest. straight into the lobby as a guest.
- **Reusable sign-in sheet:** a bottom **detented sheet** (same idiom as the comms sheet) with the - **Reusable sign-in sheet:** a bottom **detented sheet** (same idiom as the comms sheet) with the
@@ -557,7 +552,7 @@ emerge while implementing the vertical slice, step 3.)*
#### B. Onboarding — detail (TipKit, native) #### B. Onboarding — detail (TipKit, native)
**UX decisions [FIXED]** **UX decisions**
- **TipKit, native flavour** (confirmed): contextual tips anchored to controls, ordered via **`TipGroup`** - **TipKit, native flavour** (confirmed): contextual tips anchored to controls, ordered via **`TipGroup`**
**no full-screen dimming, no tap-anywhere-advance**; each tip dismisses itself (its ✕ or performing **no full-screen dimming, no tap-anywhere-advance**; each tip dismisses itself (its ✕ or performing
the action). Shown once; per-device eligibility. the action). Shown once; per-device eligibility.
@@ -568,7 +563,7 @@ emerge while implementing the vertical slice, step 3.)*
#### G. Friends — detail (Settings → Friends) #### G. Friends — detail (Settings → Friends)
**UX decisions [FIXED]** **UX decisions**
- **Structure:** a single **inset-grouped `List`** with sections **Requests** (incoming: accept / - **Structure:** a single **inset-grouped `List`** with sections **Requests** (incoming: accept /
decline; outgoing: cancel) · **Friends** · **Blocked** (unblock). A toolbar **"+"** opens the add decline; outgoing: cancel) · **Friends** · **Blocked** (unblock). A toolbar **"+"** opens the add
sheet. sheet.
@@ -587,7 +582,7 @@ emerge while implementing the vertical slice, step 3.)*
#### H. Profile & settings — detail #### H. Profile & settings — detail
**UX decisions [FIXED]** **UX decisions**
- **Settings hub** = an icon **`List` of drill-down `NavigationLink` rows** (the Podcasts → Library - **Settings hub** = an icon **`List` of drill-down `NavigationLink` rows** (the Podcasts → Library
pattern), SF-symbol icons, with room left at the bottom for optional general info later. Rows: pattern), SF-symbol icons, with room left at the bottom for optional general info later. Rows:
- **Interface** (⚙️) → display-settings screen. - **Interface** (⚙️) → display-settings screen.
@@ -602,12 +597,12 @@ emerge while implementing the vertical slice, step 3.)*
- **Interface screen** (display prefs): language · theme · board bonus-label style · reduce-motion · - **Interface screen** (display prefs): language · theme · board bonus-label style · reduce-motion ·
zoom-the-board. zoom-the-board.
**Guest has no editable settings** [FIXED] — the random nick is not changeable, the variant is always **Guest has no editable settings** — the random nick is not changeable, the variant is always
`erudite_ru`, the timezone is auto-detected (never set by hand). So there is **no guest profile `erudite_ru`, the timezone is auto-detected (never set by hand). So there is **no guest profile
screen**: the profile icon for a guest opens the **sign-in sheet** (confirms C). It also confirms E's screen**: the profile icon for a guest opens the **sign-in sheet** (confirms C). It also confirms E's
"variant picker hidden when a single variant is enabled" for guests. "variant picker hidden when a single variant is enabled" for guests.
**Profile screen (signed-in / durable)** [FIXED] — an inset-grouped `Form`: **Profile screen (signed-in / durable)** — an inset-grouped `Form`:
- **Name** — inline `TextField` + validation (letters + `·`/`.`/`_` separator, optional trailing `.` - **Name** — inline `TextField` + validation (letters + `·`/`.`/`_` separator, optional trailing `.`
or ≤5 digits, ≤32 chars, ≤5 specials). or ≤5 digits, ≤32 chars, ≤5 specials).
- **Play** — timezone (UTC-offset picker, pre-filled from the device) · away window (10-min grid, ≤12 h, - **Play** — timezone (UTC-offset picker, pre-filled from the device) · away window (10-min grid, ≤12 h,
@@ -622,7 +617,7 @@ screen**: the profile icon for a guest opens the **sign-in sheet** (confirms C).
#### I. Wallet & store — detail (durable only; delivery staged) #### I. Wallet & store — detail (durable only; delivery staged)
**UX decisions [FIXED]** **UX decisions**
- **Structure:** inset-grouped — a **balance card** (running-context «Фишка» first, then linked - **Structure:** inset-grouped — a **balance card** (running-context «Фишка» first, then linked
other-platform chips behind their logos: VK / Telegram / web / **Apple** as a new context) → **Active** other-platform chips behind their logos: VK / Telegram / web / **Apple** as a new context) → **Active**
line (hints remaining + ad-free end date / "forever"; hidden when empty) → segmented **`[Buy | Spend]`** line (hints remaining + ad-free end date / "forever"; hidden when empty) → segmented **`[Buy | Spend]`**
@@ -644,7 +639,7 @@ screen**: the profile icon for a guest opens the **sign-in sheet** (confirms C).
#### J. Feedback — detail (durable only; guests hidden) #### J. Feedback — detail (durable only; guests hidden)
**UX decisions [FIXED]** **UX decisions**
- inset-grouped `Form`: **`TextEditor`** (≤1024, live counter) → attachment row → **Send**. - inset-grouped `Form`: **`TextEditor`** (≤1024, live counter) → attachment row → **Send**.
- **Attachment:** one file ≤1 MB (images / PDF / text-log / office / RTF / archives). **"Attach"** → - **Attachment:** one file ≤1 MB (images / PDF / text-log / office / RTF / archives). **"Attach"** →
menu **`PhotosPicker` (Photos)** / **`fileImporter` (Files)**; **no camera**. An unsupported file is menu **`PhotosPicker` (Photos)** / **`fileImporter` (Files)**; **no camera**. An unsupported file is
@@ -658,7 +653,7 @@ screen**: the profile icon for a guest opens the **sign-in sheet** (confirms C).
#### K. Info / legal — detail #### K. Info / legal — detail
**UX decisions [FIXED]** **UX decisions**
- **About screen** (Info hub row): app name · **version** (native build + the client-version) · links to - **About screen** (Info hub row): app name · **version** (native build + the client-version) · links to
the three legal docs · copyright; optional "Rate on the App Store". the three legal docs · copyright; optional "Rate on the App Store".
- **Legal docs** (EULA `/eula/`, privacy `/privacy/`, offer `/offer/` + live price list): open via - **Legal docs** (EULA `/eula/`, privacy `/privacy/`, offer `/offer/` + live price list): open via
@@ -668,7 +663,7 @@ screen**: the profile icon for a guest opens the **sign-in sheet** (confirms C).
#### A. Boot / launch & update — detail #### A. Boot / launch & update — detail
**UX decisions [FIXED]** **UX decisions**
- **LaunchScreen:** static, OS-level, instant (**iOS cannot animate the launch screen**) — brand/logo. - **LaunchScreen:** static, OS-level, instant (**iOS cannot animate the launch screen**) — brand/logo.
- **In-app loading:** the crossword-tiles splash (ЭРУДИТ/ЗАГРУЗКА/ОЖИДАНИЕ) shows **only while the lobby - **In-app loading:** the crossword-tiles splash (ЭРУДИТ/ЗАГРУЗКА/ОЖИДАНИЕ) shows **only while the lobby
list isn't ready**; fast silent boot → usually straight to the lobby. Offline cold launch → straight to list isn't ready**; fast silent boot → usually straight to the lobby. Offline cold launch → straight to
@@ -684,7 +679,7 @@ screen**: the profile icon for a guest opens the **sign-in sheet** (confirms C).
#### L. Advertising banner — detail (cross-cutting component) #### L. Advertising banner — detail (cross-cutting component)
**UX decisions [FIXED]** **UX decisions**
- **Placement:** a one-line strip **on the main tabs (Lobby / New Game / Statistics) AND in-game (F)** - **Placement:** a one-line strip **on the main tabs (Lobby / New Game / Statistics) AND in-game (F)**
players spend most time in-game. Hidden in settings drill-downs and modal flows. **In-game it occupies players spend most time in-game. Hidden in settings drill-downs and modal flows. **In-game it occupies
the top strip** (the empty space above the seats header), coexisting with the connection chrome the top strip** (the empty space above the seats header), coexisting with the connection chrome