| 1 | # π€ Reproducing the iOS threading issues (#580) |
| 2 | |
| 3 | Working notes for validating the fixes on this branch. The user-visible hang is |
| 4 | intermittent because it needs a main-thread stall to coincide with a tap β so |
| 5 | instead of chasing the rare hang, reproduce the **mechanism signals** (console |
| 6 | warnings), which fire far more reliably, and treat "signal gone" as the pass |
| 7 | condition. |
| 8 | |
| 9 | ## The signals to watch (device console β Xcode debug console or Console.app) |
| 10 | |
| 11 | | Signal | What it means | |
| 12 | |---|---| |
| 13 | | `onChange(of: CGRect) action tried to update multiple times per frame` | The `globalFrame` layout feedback loop in image/link/YouTube bubbles (`MessageBubble.swift`, `YouTubeCard.swift`, `ChatView.swift`). Main-thread layout churn. | |
| 14 | | `System gesture gate timed out` | The system back/edge gesture waited on the feed's gesture stack (per-row swipe-to-reply + long-press + iOS-26 content-pop) and gave up. | |
| 15 | | `Got a keyboard will change frame / will hide notification, but keyboard was not even present` | Spurious keyboard notifications β tied to the slow-to-focus symptom. | |
| 16 | |
| 17 | Filter Console.app on the app process with text `multiple times per frame` or |
| 18 | `gesture gate`. |
| 19 | |
| 20 | ## Setup that maximizes the odds |
| 21 | |
| 22 | - A space with **many chats** (warming starts 5 live feed-model sockets). |
| 23 | - Open a chat that has **images and/or a link-preview or YouTube card** on |
| 24 | screen (those bubbles carry the `globalFrame` tracker that fuels the loop). |
| 25 | - A **second device / the web client** generating churn into the space: flap |
| 26 | presence (foreground/background), type, send messages. |
| 27 | - Use the **slowest device** available β main-thread stalls are amplified there. |
| 28 | |
| 29 | ## Per-symptom recipe |
| 30 | |
| 31 | 1. **Layout loop** β open the media chat; raise/dismiss the keyboard and scroll, |
| 32 | repeatedly. Expect the `onChange(of: CGRect)` warning to spam (near |
| 33 | deterministic). |
| 34 | 2. **Back delay** β with churn active, open the media chat and immediately tap |
| 35 | back; repeat ~20Γ. Watch for the visible delay + `System gesture gate`. |
| 36 | 3. **Phantom second copy** (the clearest one) β tap a chat row; if nothing |
| 37 | commits, tap it again a few times; when it finally opens, hit back. On the |
| 38 | buggy build "back" lands on a duplicate copy of the same chat. |
| 39 | 4. **Blank feed** β from the list, tap the media chat β immediately back β |
| 40 | immediately tap it again, ~10Γ fast. Feed opens empty; a tiny scroll redraws. |
| 41 | 5. **Slow focus** β open the media chat, immediately tap the composer. Delay |
| 42 | before the caret/keyboard; correlate with the CGRect warning. |
| 43 | |
| 44 | ## Fixes on this branch |
| 45 | |
| 46 | - **Phantom second copy (#580, symptom 6):** navigation push is now idempotent β |
| 47 | `ChatNavGuard.shouldPush` skips a push when that chat is already on top, so |
| 48 | queued taps can't stack duplicate `.thread(id)` entries |
| 49 | (`ThreadListView.onSelect`). Deterministic regression test: |
| 50 | `ChatNavGuardTests`. This makes navigation robust *to* a stall; it does not |
| 51 | remove the stall. |
| 52 | |
| 53 | - **`onChange(of: CGRect)` layout churn (likely a root-cause contributor):** the |
| 54 | image / video / YouTube cards tracked their `.global` frame with a |
| 55 | `GeometryReader` + `onChange`. That's a *continuous observation of a value |
| 56 | that changes on every layout pass* β and a chat open runs several layout |
| 57 | passes per displayed frame (scroll-settle pin, avatar spring, composer inset, |
| 58 | keyboard), so the frame changes several times per frame and SwiftUI flags |
| 59 | `onChange(of: CGRect) action tried to update multiple times per frame`. |
| 60 | (Not a self-invalidation loop β moving the write off `@State` did NOT silence |
| 61 | it, confirmed by breakpoint at `MessageBubble` image bubble: the trigger is |
| 62 | observing a high-frequency value at all.) The frame is read **only at tap |
| 63 | time** and **only on macOS** (the lightbox zooms out from it; iOS ignores it β |
| 64 | every `onImageTap` consumer takes `_`), so the tracker is compiled out on iOS |
| 65 | via `trackingFrame(into:)` and kept on macOS. **Validate via the console |
| 66 | signal** β the warning should no longer appear while scrolling / animating the |
| 67 | keyboard in a media chat on iOS (runtime behaviour the test suite can't cover). |
| 68 | |
| 69 | _Still to land (tracked, not yet on this branch): apply `EdgeOnlyBackGesture` |
| 70 | to `NativeChatScreen` on iOS 26; coalesce presence frames / skip no-op row |
| 71 | reconfigures; make the feed reveal robust to a missed opening pin (blank |
| 72 | feed)._ |
| 73 | |