Skip to content
Source
Sign inSign up
Open History to see the last commit

mobile-navigation.md

92 lines5.3 KB

1# Mobile navigation
2​
3Home and thread routes share the [root native stack](../../apps/mobile/src/Stack.tsx)
4so UIKit can animate the header within one navigation controller. The iPad sidebar
5has its own stack. Splitting Home and threads between controllers loses that
6continuous transition. How the stack becomes columns, sheets and panes is in
7[Mobile layout](./mobile-layout.md).
8​
9## Selecting versus drilling in
10​
11Opening a page has two intents, and each has one action:
12​
13- **Select** shows a thread or pull request as the workspace's only page, the
14 way a list row does. Everything that selects (rows, the command palette,
15 compose, notifications, deep links) uses
16 [`selectWorkspaceDetail`](../../apps/mobile/src/features/layout/workspace/workspace-detail-selection.ts),
17 which the root stack's router handles. The resulting stack is the same on
18 phones, collapsed splits, and expanded splits. Only route keys differ: the
19 page already showing keeps its key, so the detail column updates in place
20 instead of pushing.
21- **Drill in** opens a page over the current one so Back returns, with
22 `StackActions.push`.
23​
24`navigate` to `Thread` or `PullRequest` is rejected by lint, because it pushes
25or updates in place depending on what happens to be on top. Never infer the
26choice from transition events: UIKit reports the sidebar column appearing in
27split views too. The one native signal the router uses is the card native Back
28is popping. The workspace navigator reports it, and a selection never reuses
29that card.
30​
31Beside the list, Settings pages share one native screen whose content swaps,
32like the system Settings app. A route per page would pop and push the column,
33and would unmount its header configuration, which hides the navigation bar.
34​
35## UIKit constraints
36​
37The [react-native-screens fork](../operations/mobile-screens-fork.md)
38preserves behavior that is easy to break when changing native headers:
39​
40- The brand belongs in `headerTitle`. On iOS 26.5, UIKit morphs a background-free
41 leading toolbar item's rectangle into the next screen's glass back button,
42 even when the items have different identifiers.
43- UIKit's scroll-edge fade does not recognize Fabric text views. An empty native
44 label supplies the custom title's geometry. Removing that apparently unused
45 view changes the fade.
46- Leading, trailing, and center item groups need independent caches. A button can
47 belong to only one group; rebuilding an unchanged group can pull its buttons
48 out of a group UIKit is still animating. Cache ownership also includes the
49 header config, whose event emitter changes on remount.
50- A horizontal ScrollView at its leading edge must yield to the full-screen back
51 gesture. Upstream gives every horizontal ScrollView priority, so swiping back
52 on a code block or table can only bounce its content.
53- UIKit draws the header's scroll-edge blur inside the scroll view, and workspace
54 columns pad their content away from the iPhone Duo's vertical bar. Register a
55 header scroll view only through
56 [`NativeScrollViewMarker`](../../apps/mobile/src/native/NativeScrollViewMarker.tsx)
57 or `NativeScrollView`: their native wrapper widens the scroll view under that
58 bar, so the blur reaches it while the content stays in the safe area. On the
59 left it shifts React Native's content view back through its bounds origin,
60 never with a content offset: React Native rewrites offsets on its own layout
61 passes, which left threads shifted under the bar. Lint rejects
62 `ScrollViewMarker` imported anywhere else.
63​
64The fork adds native header props that require code generation and a new binary.
65See the [fork build instructions](../operations/mobile-screens-fork.md).
66​
67## Native media presentations
68​
69iOS delegates full-screen video to [AVKit](../../apps/mobile/modules/t3-native-controls/ios/T3NativeVideoPresentation.swift)
70and images and documents to [Quick Look](../../apps/mobile/modules/t3-native-controls/ios/T3NativeFilePresentation.swift).
71Each framework owns its controls and transition. A separate UIKit zoom transition
72prevents AVKit's native Close action from exiting full screen and interferes with
73Quick Look's interactive return to its thumbnail. The AVKit entry selector is
74guarded, with standard modal presentation as the fallback.
75​
76[Thumbnail registration](../../apps/mobile/modules/t3-native-controls/ios/T3NativePresentation.swift)
77holds weak view references for transition and share-sheet anchors. It does not own
78the preview; a source row can disappear while a native presentation is open.
79Identifiers must distinguish attachments that are visible at the same time.
80Native promises complete after dismissal so callers keep local file leases until
81the preview or share flow has finished.
82​
83Treat a dismissal request as pending until UIKit finishes its current transition.
84Starting another dismissal while presentation or an interactive dismissal is
85settling can strand the controller. Video cleanup restores the previous audio
86configuration only if it still matches the preview's configuration. It must not
87deactivate the shared audio session, which may belong to another player or recorder.
88​
89Video playback holds the first signed asset URL for the lifetime of the preview.
90Following credential refreshes reactively would restart playback. Quick Look copies
91original bytes into its own temporary directory so preview and sharing cannot
92mutate a draft or workspace file.