@apexnative/navigation 1.0.0-alpha.8 → 1.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/android/src/main/java/dev/apexnative/navigation/options/SideMenuOptions.kt +6 -1
  3. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +36 -5
  4. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -0
  5. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +35 -3
  6. package/android/src/main/java/dev/apexnative/navigation/views/sidemenu/SideMenuView.kt +12 -3
  7. package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +34 -4
  8. package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +6 -3
  9. package/docs-md/containers/side-menu.md +15 -0
  10. package/docs-md/containers/tabs.md +1 -1
  11. package/docs-md/index.json +4 -0
  12. package/ios/CLAUDE.md +4 -1
  13. package/ios/CORE.md +2 -2
  14. package/ios/Core/ANNLayoutProtocol.h +5 -5
  15. package/ios/Core/ANNParking.h +5 -4
  16. package/ios/Core/ANNReloadCoordinator.mm +1 -4
  17. package/ios/Core/ANNRenderWait.h +5 -5
  18. package/ios/Core/ANNRenderWait.mm +24 -39
  19. package/ios/Core/ApexNavigation.mm +1 -4
  20. package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
  21. package/ios/Layouts/Overlay/ANNOverlayCoordinator.mm +1 -1
  22. package/ios/Layouts/Root/ANNRootCoordinator.mm +1 -1
  23. package/ios/Layouts/Screen/ANNComponentViewController.h +9 -15
  24. package/ios/Layouts/Screen/ANNComponentViewController.mm +6 -42
  25. package/ios/Layouts/Screen/ANNScreenPlace.h +12 -0
  26. package/ios/Layouts/Screen/ANNScreenPlace.mm +46 -16
  27. package/ios/Layouts/SideMenu/ANNSideMenuTypes.h +8 -0
  28. package/ios/Layouts/SideMenu/ANNSideMenuViewController.mm +83 -14
  29. package/ios/Layouts/Stack/ANNStackController.mm +5 -2
  30. package/ios/Layouts/Stack/ANNStackQueue.h +2 -1
  31. package/ios/Layouts/Tabs/ANNBottomTabsController.h +3 -0
  32. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +222 -51
  33. package/ios/Options/ANNSideMenuOptions.h +2 -0
  34. package/ios/Options/ANNSideMenuOptions.mm +2 -0
  35. package/ios/React/ANNSafeAreaViewComponentView.mm +2 -2
  36. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -1
  37. package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
  38. package/lib/typescript/interfaces/options/sideMenu.d.ts +16 -0
  39. package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/interfaces/options/bottomTabs.ts +2 -1
  42. package/src/interfaces/options/sideMenu.ts +17 -0
  43. package/ios/Layouts/Tabs/ANNTabsPreloadWindow.h +0 -27
  44. package/ios/Layouts/Tabs/ANNTabsPreloadWindow.mm +0 -53
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0-alpha.9](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.8...v1.0.0-alpha.9) (2026-10-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * side menus can be created after the center or at their first open ([#67](https://github.com/apexnative/navigation/issues/67)) ([a4a0ecf](https://github.com/apexnative/navigation/commit/a4a0ecfdc04ccd13d2c96b5799b45e510113a7e8))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **android:** the room beside a moving screen goes on with what its edge shows ([#64](https://github.com/apexnative/navigation/issues/64)) ([eb0936f](https://github.com/apexnative/navigation/commit/eb0936f15d8d157d31621917920814df2c725ff1))
14
+ * **ios:** tabs not shown yet render their screens before they show ([#68](https://github.com/apexnative/navigation/issues/68)) ([cf34a09](https://github.com/apexnative/navigation/commit/cf34a09a772865a8e3ff377b053c968e306f4972))
15
+
3
16
  ## [1.0.0-alpha.8](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.7...v1.0.0-alpha.8) (2026-10-29)
4
17
 
5
18
 
@@ -20,16 +20,18 @@ class SideMenuOptions {
20
20
  @JvmField var right = SideMenuSideOptions()
21
21
  @JvmField var openGestureMode: Text = NullText()
22
22
  @JvmField var animationType: Text = NullText()
23
+ @JvmField var menusAttachMode: Text = NullText()
23
24
  @JvmField var testID: Text = NullText()
24
25
 
25
26
  fun hasValue(): Boolean = left.hasValue() || right.hasValue() || openGestureMode.hasValue() ||
26
- animationType.hasValue() || testID.hasValue()
27
+ animationType.hasValue() || menusAttachMode.hasValue() || testID.hasValue()
27
28
 
28
29
  fun mergeWith(other: SideMenuOptions) {
29
30
  left.mergeWith(other.left)
30
31
  right.mergeWith(other.right)
31
32
  if (other.openGestureMode.hasValue()) openGestureMode = other.openGestureMode
32
33
  if (other.animationType.hasValue()) animationType = other.animationType
34
+ if (other.menusAttachMode.hasValue()) menusAttachMode = other.menusAttachMode
33
35
  if (other.testID.hasValue()) testID = other.testID
34
36
  }
35
37
 
@@ -38,6 +40,7 @@ class SideMenuOptions {
38
40
  right.mergeWithDefault(defaults.right)
39
41
  if (!openGestureMode.hasValue()) openGestureMode = defaults.openGestureMode
40
42
  if (!animationType.hasValue()) animationType = defaults.animationType
43
+ if (!menusAttachMode.hasValue()) menusAttachMode = defaults.menusAttachMode
41
44
  if (!testID.hasValue()) testID = defaults.testID
42
45
  }
43
46
 
@@ -46,6 +49,7 @@ class SideMenuOptions {
46
49
  companion object {
47
50
  private val OPEN_GESTURE_MODES = setOf("bezel", "entireScreen")
48
51
  private val ANIMATION_TYPES = setOf("parallax", "slide", "slide-and-scale", "door")
52
+ private val MENUS_ATTACH_MODES = setOf("together", "afterCenter", "onOpen")
49
53
 
50
54
  @JvmStatic
51
55
  fun parse(map: ReadableMap?): SideMenuOptions {
@@ -55,6 +59,7 @@ class SideMenuOptions {
55
59
  options.right = map.group("right") { SideMenuSideOptions.parse(it) }
56
60
  options.openGestureMode = TextParser.parse(map, "openGestureMode", OPEN_GESTURE_MODES)
57
61
  options.animationType = TextParser.parse(map, "animationType", ANIMATION_TYPES)
62
+ options.menusAttachMode = TextParser.parse(map, "menusAttachMode", MENUS_ATTACH_MODES)
58
63
  options.testID = TextParser.parse(map, "testID")
59
64
  return options
60
65
  }
@@ -29,7 +29,8 @@ import dev.apexnative.navigation.views.sidemenu.SideMenuView
29
29
  * - The current child is the open menu's node, else the center ([SideMenuMotion.openSide]: from the end of the
30
30
  * opening to the end of the closing); the state, the options and back follow it.
31
31
  * - The center and the open menu show with the side menu ([visibleChildren]); a wait for the side menu to render
32
- * waits for the center ([waitedChildren]); the menus' views are made with the side menu's.
32
+ * waits for the center ([waitedChildren]); the menus' views join the drawer as `sideMenu.menusAttachMode` says
33
+ * ([attached]): with the side menu's view, once it first appeared, or each as it first starts to show.
33
34
  * - Its options ([SideMenuPresenter]): from the resolution of [resolveShownOptions] at creation, at every appearance
34
35
  * of a screen inside, and for the fields a merge names; `sideMenu.<side>.visible` acts once, from a merge or from the
35
36
  * layout at the first show. What goes on up goes without `sideMenu`: the nearest side menu acts.
@@ -64,6 +65,12 @@ internal class SideMenuController(
64
65
  // From the end of its appearance's start until it disappears: the drawer's motion tells the menu screens then.
65
66
  private var appeared = false
66
67
  private var firstShown = false
68
+ private var firstAppeared = false
69
+
70
+ private val attachMode = SideMenuRules.attachModeOf(initialOptions.sideMenu, DefaultOptions.current.sideMenu)
71
+
72
+ /** The menus whose nodes' views are in the drawer, or join it as it is made. */
73
+ internal val attached = HashSet<Side>(2)
67
74
  // Its children go first as it is destroyed: nothing reacts to them, nor to the drawer, from the start.
68
75
  private var destroying = false
69
76
 
@@ -72,6 +79,7 @@ internal class SideMenuController(
72
79
 
73
80
  init {
74
81
  getChildControllers().forEach { it.parentController = this }
82
+ SideMenuRules.attachedAtCreation(attachMode).filter { childOf(it) != null }.forEach { attached += it }
75
83
  }
76
84
 
77
85
  override fun getChildControllers(): List<ViewController<*>> = listOfNotNull(center, left, right)
@@ -88,13 +96,16 @@ internal class SideMenuController(
88
96
 
89
97
  private fun childOf(side: Side): ViewController<*>? = if (side == Side.Left) left else right
90
98
 
91
- // The drawer and its parts, each node's view in its part; the creation's options. An open menu recorded before the
92
- // view existed is open in it from the start; the drawer is silent before its first layout.
99
+ // The drawer and its parts, the view of each node attached so far in its part; the creation's options. An open menu
100
+ // recorded before the view existed is open in it from the start; the drawer is silent before its first layout.
93
101
  override fun createView(): SideMenuView {
94
102
  val view = SideMenuView(activity)
95
103
  view.setCenter(center.view)
96
- left?.let { view.addPart(Side.Left, it.view) }
97
- right?.let { view.addPart(Side.Right, it.view) }
104
+ for (side in Side.entries) {
105
+ val child = childOf(side) ?: continue
106
+ view.addPart(side)
107
+ if (side in attached) view.fillPart(side, child.view)
108
+ }
98
109
  menus.applyOnCreation(view, resolveShownOptions(DefaultOptions.current))
99
110
  motion.openSide?.let { view.open(it, false) }
100
111
  view.follow(motion)
@@ -172,6 +183,14 @@ internal class SideMenuController(
172
183
  motion.containerAppeared()
173
184
  }
174
185
 
186
+ // `afterCenter`: the menus join once the side menu first appeared with its center.
187
+ override fun onViewDidAppear() {
188
+ super.onViewDidAppear()
189
+ if (firstAppeared || destroying || isDestroyed) return
190
+ firstAppeared = true
191
+ SideMenuRules.attachedAfterAppearance(attachMode).forEach(::attach)
192
+ }
193
+
175
194
  override fun onViewDisappear() {
176
195
  appeared = false
177
196
  motion.containerDisappeared()
@@ -222,6 +241,16 @@ internal class SideMenuController(
222
241
  if (child === currentChild) super.tabBarSettled(child)
223
242
  }
224
243
 
244
+ // The view of the menu on [side] joins its part, once, before its screens hear that they appear: a stack lays out
245
+ // its bar as it appears in the window. The drawer's first movement may come while it draws (a peek settles in
246
+ // `computeScroll`, before the drawer draws its children): the view joins the part then, measured and laid out at the
247
+ // next frame.
248
+ private fun attach(side: Side) {
249
+ val child = childOf(side) ?: return
250
+ if (destroying || isDestroyed || !attached.add(side)) return
251
+ peekView()?.fillPart(side, child.view)
252
+ }
253
+
225
254
  /** Destroys the side menu once: the drawer's callbacks stop first, then the children go. */
226
255
  override fun destroy() {
227
256
  if (isDestroyed) return
@@ -238,6 +267,8 @@ internal class SideMenuController(
238
267
 
239
268
  override fun child(side: Side): ViewController<*>? = childOf(side)
240
269
 
270
+ override fun starts(side: Side) = attach(side)
271
+
241
272
  override fun isEnabled(side: Side): Boolean =
242
273
  SideMenuRules.isEnabled(resolveShownOptions(DefaultOptions.current).sideMenu, side)
243
274
 
@@ -82,6 +82,12 @@ internal class SideMenuMotion(
82
82
  /** `sideMenu.<side>.enabled` of the side menu's options now. */
83
83
  fun isEnabled(side: Side): Boolean
84
84
 
85
+ /**
86
+ * The menu on [side] starts to show: the drawer's first movement, or a command that opens it. Before its
87
+ * screen hears that it appears.
88
+ */
89
+ fun starts(side: Side)
90
+
85
91
  /** `visible: true` for a side the layout doesn't have: a warning, nothing else. */
86
92
  fun missingSide(side: Side)
87
93
 
@@ -122,6 +128,7 @@ internal class SideMenuMotion(
122
128
  fun slid(side: Side, offset: Float) {
123
129
  // Not more than 0 also takes a NaN: `DrawerLayout` divides by the drawer's width, which may be 0.
124
130
  if (destroyed || !(offset > 0f)) return
131
+ if (!isOnScreen(side)) host.starts(side)
125
132
  onScreen += side
126
133
  if (side in appearing || !host.appeared) return
127
134
  appearing += side
@@ -207,6 +214,7 @@ internal class SideMenuMotion(
207
214
  }
208
215
  // Never decided by where the other menu is: a side asked to open in the same merge hasn't moved yet.
209
216
  if (drawer.has(side.other)) move(side.other, open = false, animate = animated)
217
+ if (!isOnScreen(side)) host.starts(side)
210
218
  onScreen += side
211
219
  // A locked menu opened from code can be dragged closed while it is open.
212
220
  drawer.lock(side, locked = false)
@@ -11,11 +11,23 @@ import kotlin.math.min
11
11
 
12
12
  /**
13
13
  * What the options of a side menu do on Android (`sideMenu.*`), without views: the values of each moment, `visible` as
14
- * a command, the sizes, the `testID`s. Android reads `enabled`, `width`, `height`, `visible`, `animate` and the
15
- * `testID`s of `sideMenu`; `openGestureMode`, `animationType`, `openMode`, `shouldStretchDrawer`, `animationVelocity`
16
- * and `glass` are iOS's.
14
+ * a command, the sizes, the `testID`s, when the menus join the drawer. Android reads `enabled`, `width`, `height`,
15
+ * `visible`, `animate`, `menusAttachMode` and the `testID`s of `sideMenu`; `openGestureMode`, `animationType`,
16
+ * `openMode`, `shouldStretchDrawer`, `animationVelocity` and `glass` are iOS's.
17
17
  */
18
18
  internal object SideMenuRules {
19
+ /** When the views of the menus' nodes join the drawer (`sideMenu.menusAttachMode`): their content renders then. */
20
+ enum class AttachMode {
21
+ /** With the side menu's view. */
22
+ Together,
23
+
24
+ /** Once the side menu first appeared, with its center. */
25
+ AfterCenter,
26
+
27
+ /** Each as it first starts to show: the drawer's first movement, or a command that opens it. */
28
+ OnOpen,
29
+ }
30
+
19
31
  /** A side's `visible` of one merge: open or close it, with its `animate`. */
20
32
  data class Command(val side: Side, val visible: Boolean, val animate: Boolean)
21
33
 
@@ -43,6 +55,26 @@ internal object SideMenuRules {
43
55
  */
44
56
  fun firstShow(layout: SideMenuOptions): List<Side> = Side.entries.filter { sideOf(layout, it).visible.isTrue }
45
57
 
58
+ /**
59
+ * The mode of `sideMenu.menusAttachMode`: the side menu's [own] options, else the [defaults]; `together` without
60
+ * either. Read once, as the side menu is made.
61
+ */
62
+ fun attachModeOf(own: SideMenuOptions, defaults: SideMenuOptions): AttachMode {
63
+ val value = own.menusAttachMode.takeIf { it.hasValue() } ?: defaults.menusAttachMode
64
+ return when (value.get(null)) {
65
+ "afterCenter" -> AttachMode.AfterCenter
66
+ "onOpen" -> AttachMode.OnOpen
67
+ else -> AttachMode.Together
68
+ }
69
+ }
70
+
71
+ /** The menus that join as the side menu is made: both with `together`. */
72
+ fun attachedAtCreation(mode: AttachMode): List<Side> = if (mode == AttachMode.Together) Side.entries else emptyList()
73
+
74
+ /** The menus that join once the side menu first appeared: both with `afterCenter`. */
75
+ fun attachedAfterAppearance(mode: AttachMode): List<Side> =
76
+ if (mode == AttachMode.AfterCenter) Side.entries else emptyList()
77
+
46
78
  /** `enabled` of [side]: true unless `false`, from the side menu's resolved options. */
47
79
  fun isEnabled(options: SideMenuOptions, side: Side): Boolean = !sideOf(options, side).enabled.isFalse
48
80
 
@@ -48,15 +48,24 @@ internal class SideMenuView(context: Context) : DrawerLayout(context), SideMenuM
48
48
  center.addView(content, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
49
49
  }
50
50
 
51
- /** The part of [side], holding [content]: a drawer at that edge, the whole height of the side menu. */
52
- fun addPart(side: Side, content: View): FrameLayout {
51
+ /**
52
+ * The part of [side], empty until [fillPart]: a drawer at that edge, the whole height of the side menu. It opens,
53
+ * closes and takes its width and lock without its content.
54
+ */
55
+ fun addPart(side: Side): FrameLayout {
53
56
  val part = FrameLayout(context)
54
- part.addView(content, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
55
57
  addView(part, LayoutParams(MATCH_PARENT, MATCH_PARENT, gravityOf(side)))
56
58
  parts[side.ordinal] = part
57
59
  return part
58
60
  }
59
61
 
62
+ /** [content] fills the part of [side], once. */
63
+ fun fillPart(side: Side, content: View) {
64
+ val part = partOf(side) ?: return
65
+ if (content.parent === part) return
66
+ part.addView(content, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
67
+ }
68
+
60
69
  /** The part of [side]; none when the layout has no menu there. */
61
70
  fun partOf(side: Side): FrameLayout? = parts[side.ordinal]
62
71
 
@@ -1,15 +1,45 @@
1
1
  package dev.apexnative.navigation.views.stack
2
2
 
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Canvas
5
+ import android.graphics.drawable.BitmapDrawable
3
6
  import android.graphics.drawable.Drawable
4
7
  import android.view.View
5
8
 
6
9
  /**
7
10
  * What fills the room beside a moving screen in a transition (`StackLayout.extendEdge`), where the system's
8
11
  * transitions between activities stretch a window's edge (`<extend>` of `activity_open_*` and `activity_close_*`): the
9
- * screen's own background, which is what its edge shows unless its content covers it. Taken without drawing the
10
- * screen, at the start of every push and pop. Main thread only.
12
+ * column of pixels at the screen's edge, stretched across the room, as the system stretches the window's. What the
13
+ * screen shows there goes on beside it — a gradient its content draws, not only its own background. Taken at the start
14
+ * of every push and pop. Main thread only.
11
15
  */
12
16
  internal object ScreenEdge {
13
- /** A copy of [view]'s background, stretched across the room; none without one. */
14
- fun pictureOf(view: View): Drawable? = view.background?.constantState?.newDrawable(view.resources)?.mutate()
17
+ /**
18
+ * The column of pixels at [view]'s left edge ([left]) or right edge, made to be stretched; its background when the
19
+ * screen isn't laid out as it shows ([laidOut]: not after a rotation while it was out of the stack) or can't be drawn
20
+ * in software (a `HARDWARE` bitmap in it, a view that fails to draw), none without one either.
21
+ */
22
+ fun pictureOf(view: View, left: Boolean, laidOut: Boolean): Drawable? =
23
+ (if (laidOut) columnOf(view, left) else null) ?: backgroundOf(view)
24
+
25
+ // One pixel wide and as tall as the screen: the view draws itself into a canvas that keeps that column only. Any
26
+ // view of the screen may fail to draw in software (a hardware bitmap, a view that wants a hardware canvas): the
27
+ // screen's background stands in then, as the transition must go on.
28
+ private fun columnOf(view: View, left: Boolean): Drawable? {
29
+ val width = view.width
30
+ val height = view.height
31
+ if (width <= 0 || height <= 0) return null
32
+ return try {
33
+ val column = Bitmap.createBitmap(1, height, Bitmap.Config.ARGB_8888)
34
+ val canvas = Canvas(column)
35
+ if (!left) canvas.translate(-(width - 1).toFloat(), 0f)
36
+ view.draw(canvas)
37
+ BitmapDrawable(view.resources, column)
38
+ } catch (e: RuntimeException) {
39
+ null
40
+ }
41
+ }
42
+
43
+ private fun backgroundOf(view: View): Drawable? =
44
+ view.background?.constantState?.newDrawable(view.resources)?.mutate()
15
45
  }
@@ -145,12 +145,15 @@ class StackLayout(context: Context) : CoordinatorLayout(context) {
145
145
 
146
146
  /**
147
147
  * [view] extends its start edge ([atStart]) or its end edge by [width] pixels while it moves, as the system's
148
- * transitions between activities extend a window: its background fills the room it moves away from, so the screen
149
- * under it never shows at its edge. [dropEdge] ends it. Each frame of the move calls [edgesMoved].
148
+ * transitions between activities extend a window: the pixels at its edge, stretched, fill the room it moves away
149
+ * from, so the screen under it never shows at its edge. [dropEdge] ends it. Each frame of the move calls
150
+ * [edgesMoved].
150
151
  */
151
152
  fun extendEdge(view: View, atStart: Boolean, width: Int) {
152
153
  val left = atStart != (layoutDirection == LAYOUT_DIRECTION_RTL)
153
- val picture = ScreenEdge.pictureOf(view) ?: return
154
+ // A screen put back for a pop keeps the size of its last layout until the next one: after a rotation while it was
155
+ // out of the stack, its pixels are of another width.
156
+ val picture = ScreenEdge.pictureOf(view, left, laidOut = view.width == this.width) ?: return
154
157
  edges[view] = Edge(picture, width, left)
155
158
  invalidate()
156
159
  }
@@ -124,6 +124,21 @@ On iOS the content moves aside, and the menu comes in under it with a parallax;
124
124
 
125
125
  (iOS) 26: `left({ glass: true })` puts the menu on Liquid Glass. The glass shows where the menu's screen has no background of its own.
126
126
 
127
+ ## When the menus are created
128
+
129
+ The screens of the menus are created with the side menu by default, with their React content. A heavy menu competes
130
+ with the first render of the center. `options({ menusAttachMode })` moves the creation:
131
+
132
+ ```ts
133
+ SideMenu({ left: 'Menu', center: 'Home' }).options({ menusAttachMode: 'onOpen' });
134
+ ```
135
+
136
+ - `'together'` (the default): with the center.
137
+ - `'afterCenter'`: right after the side menu first appears. The menus don't compete with the first render of the center and are ready at the first open.
138
+ - `'onOpen'`: each menu the first time it starts to open, from a swipe, `openMenu`, `visible: true` or `visible` in the layout. Until React draws the menu, it shows its background (`backgroundColor`). A second open reuses it.
139
+
140
+ The trade-off of `'onOpen'`: the first open of a menu doesn't wait for its render and shows its background for a moment, while a menu not opened yet runs no code. A push into the stack of such a menu and `mergeOptions` on its screen wait for the menu, like a tab not yet selected. The screens of the menus get appear events only when the menu opens, in every mode. The mode is read once, when the side menu is created, from its own options, else the default options; `mergeOptions` doesn't change it. With `appProvider` the screens render in the app's React tree at once in every mode, and the mode delays only their native containers. It means the same on iOS and Android.
141
+
127
142
  ## Where the swipe is off
128
143
 
129
144
  A swipe from the edge can clash with the content, such as a map or a carousel. A screen turns it off **while it is on top of the center**, in its options:
@@ -148,7 +148,7 @@ From a screen of the tab, `setTabBadge(count)` needs no id: it sets the badge of
148
148
 
149
149
  ## When tabs are created
150
150
 
151
- By default every tab is created with the tabs, so a switch shows it at once. `Tabs().lazy()` creates each tab the first time it shows. The app starts faster when its tabs are heavy, and a tab the user never opens costs nothing.
151
+ By default every tab is created with the tabs, so a switch shows it at once. On (iOS) a text input's `autoFocus` in a tab not selected yet does nothing then: focus the field in `useScreenAppear`. `Tabs().lazy()` creates each tab the first time it shows. The app starts faster when its tabs are heavy, and a tab the user never opens costs nothing.
152
152
 
153
153
  ```ts
154
154
  const main = Tabs(/* … */).lazy();
@@ -739,6 +739,10 @@
739
739
  "heading": "How it looks",
740
740
  "anchor": "look"
741
741
  },
742
+ {
743
+ "heading": "When the menus are created",
744
+ "anchor": "attach"
745
+ },
742
746
  {
743
747
  "heading": "Where the swipe is off",
744
748
  "anchor": "swipe"
package/ios/CLAUDE.md CHANGED
@@ -20,7 +20,7 @@ Swift is not used here: apps reach the core through the public headers (`Public/
20
20
  - `Layouts/Screen/`: The screen (`ANNComponentViewController`), its presenter, readiness, its environment for JS (`ANNScreenEnvironment`), regions, safe-area math, the main list rule, previews.
21
21
  - `Layouts/External/`: Native screens: `ANNExternalViewController`, `ANNNativeScreenCalls`.
22
22
  - `Layouts/Stack/`: `ANNStackController`, `ANNStackQueue`, `ANNStackCommands`, `ANNStackPresenter`, `ANNPreviewPush`, the swipe-back delegate.
23
- - `Layouts/Tabs/`: `ANNBottomTabsController`, its presenter, item builder, custom row, preload window, More delegate, long press, dots.
23
+ - `Layouts/Tabs/`: `ANNBottomTabsController`, its presenter, item builder, custom row, More delegate, long press, dots.
24
24
  - `Layouts/Modal/`: `ANNModalCoordinator`, `ANNModal`, `ANNPresentationGate`, `ANNModalPresentation`, `ANNModalTransitions`, sheets and popovers.
25
25
  - `Layouts/Overlay/`: `ANNOverlayCoordinator`, `ANNOverlayWindow`.
26
26
  - `Layouts/SideMenu/`: The side menu module and its presenter and wrapper.
@@ -106,6 +106,9 @@ Top-level files: `Package.swift` (SwiftPM; `headerFolders` lists every folder wh
106
106
  list would move as the push starts and UIKit would show the large title only after the transition. On iOS 26 its
107
107
  large title also keeps a place of the core's for the push (`arrivesByPushOn:`). Check Showcase 1.4 (with a font
108
108
  size), every section panel and 12.6 on push, pop and swipe after any change to waits, parking or the large title.
109
+ A tab rendered before it shows keeps its parent, yet UIKit gives its views no safe area either: it renders parked
110
+ in the tabs' view with places measured in the shown tab's view, then leaves the parking, since the core takes a view
111
+ in a window for one on screen (`isInUnseenTab:`); its screens keep that place until they appear.
109
112
  - Every change of a stack's children goes through its `ANNStackQueue`. The core's own calls to UIKit's navigation
110
113
  come only from there; UIKit's own pops are followed (`uikitShows:`), not fought.
111
114
  - Every UIKit present and dismiss in the main window is a step in `ANNPresentationGate`. UIKit drops a transition
package/ios/CORE.md CHANGED
@@ -128,8 +128,8 @@ Each of these owns one rule. Call it; do not copy it.
128
128
  - `ANNNavigationState`: The visible screen and the nearest tabs it is in (the last tabs met walking down); coalesced updates, holds, the state now. Called by: The dispatcher, `ANNCommandAnswer`, `ANNRootCoordinator`, `ANNModalCoordinator`, `ANNBottomTabsController`.
129
129
  - `ANNScreenReadiness`: The signals of a screen's safe area view (has one, laid out, content rendered), sticky, by screen id. Called by: `ANNSafeAreaViewComponentView`, `ANNReactView`.
130
130
  - `ANNRenderWait`: The one wait for the render of what a command shows: content, bar components, limits, the unknown-content rule. Called by: Root, stack queue, modals, overlays, `showInColumn`.
131
- - `ANNParking`: Where a view waits for its content unseen: the window, a stack's view, a preview's host view. Called by: Root, stack, modals, overlays, `ANNPreviewPush`.
132
- - `ANNScreenPlace`: The safe area a parked screen lays out with, measured where it will show on top of a stack. Called by: `ANNStackController` (`stackPark:`), `ANNSplitViewCommands` (`showInColumn`), through `placeWhileParked` of the screen.
131
+ - `ANNParking`: Where a view waits for its content unseen: the window, a stack's view, a preview's host view, the tabs' view. Called by: Root, stack, modals, overlays, `ANNPreviewPush`, tabs (a tab rendered before it shows).
132
+ - `ANNScreenPlace`: The safe area a parked screen lays out with, measured where it will show on top of a stack. Called by: `ANNStackController` (`stackPark:`), `ANNSplitViewCommands` (`showInColumn`), the tabs (a tab rendered before it shows, measured in the shown tab's view), through `placeWhileParked` of the screen.
133
133
  - `ANNPendingLayers`: Layers that wait to show (a pending root, a modal, an overlay); a node inside counts as off screen. Called by: The layer's owner; nodes ask.
134
134
  - `ANNShownScreens`: "What a node shows changed": tells the pending layer, the modal hold and the system UI, in that order. Called by: Every container that changes what it shows.
135
135
  - `ANNModalCoordinator`, `ANNModal`: One record per modal, its states and waiting commands. Called by: Commands, root, state, repaint, reload.
@@ -11,10 +11,10 @@ NS_ASSUME_NONNULL_BEGIN
11
11
  typedef NS_ENUM(NSInteger, ANNWaitedReadiness) {
12
12
  /// The screen's content rendered: what the user sees.
13
13
  ANNWaitedReadinessContent,
14
- /// The screen's root mounted once: a screen the user doesn't see yet (an unselected tab of `together`), which
15
- /// renders its content only where it shows. Its part of a wait ends at the command's 0.5 s at the latest, and its
16
- /// bar components are not waited for.
17
- ANNWaitedReadinessRootMounted,
14
+ /// The screen's content rendered, on a screen the user doesn't see yet (an unselected tab of `together`, which
15
+ /// renders unseen in the tabs' view). Its part of a wait ends at the command's 0.5 s at the latest, and its bar
16
+ /// components are not waited for.
17
+ ANNWaitedReadinessUnseen,
18
18
  };
19
19
 
20
20
  /// A transition of the navigation is done: what it moved is in place.
@@ -97,7 +97,7 @@ typedef void (^ANNTransitionRejectionBlock)(NSString *message, NSError *_Nullabl
97
97
  - (NSArray<UIViewController *> *)waitedComponentViewControllers;
98
98
 
99
99
  /// The readiness a wait needs of `screen`, a waited screen inside this node; a node without an answer leaves it to its
100
- /// parents (`ANNWaitedReadinessOf`). The tabs answer "root mounted" for the screens of their unselected tabs.
100
+ /// parents (`ANNWaitedReadinessOf`). The tabs answer "unseen" for the screens of their unselected tabs.
101
101
  - (ANNWaitedReadiness)waitedReadinessOfScreen:(UIViewController *)screen;
102
102
 
103
103
  #pragma mark - The children
@@ -4,10 +4,11 @@ NS_ASSUME_NONNULL_BEGIN
4
4
 
5
5
  /// A place where the views of nodes wait for their content unseen (their safe area views lay out only in a window): the
6
6
  /// window under the current root while a root waits, a stack's view under UIKit's container while a screen of the stack
7
- /// waits or renders. A parked view sits at the back of its host, at the host's size and following it (a rotation, a
8
- /// resize), gets no touches and is hidden from accessibility; it isn't a child of anything there, and its node's
9
- /// appearance is driven meanwhile (`ANNAppearanceDispatcher driveAppearanceOf:`), so UIKit's appearance callbacks
10
- /// report nothing for it. Each node leaves it as it was. Main queue.
7
+ /// waits or renders, the tabs' view while a tab rendered before it shows renders its content. A parked view sits at the
8
+ /// back of its host, at the host's size and following it (a rotation, a resize), gets no touches and is hidden from
9
+ /// accessibility; it isn't a child of anything there (a tab stays the tabs' child), and its node's appearance is driven
10
+ /// meanwhile (`ANNAppearanceDispatcher driveAppearanceOf:`), so UIKit's appearance callbacks report nothing for it.
11
+ /// Each node leaves it as it was. Main queue.
11
12
  @interface ANNParking : NSObject
12
13
 
13
14
  /// `node`'s view waits in `host` from now on, with what it had kept. NO when it waits here already.
@@ -15,7 +15,6 @@
15
15
  #import "ANNScreenReadiness.h"
16
16
  #import "ANNSplashViewController.h"
17
17
  #import "ANNSplitViewCommands.h"
18
- #import "ANNTabsPreloadWindow.h"
19
18
  #import "ApexNavigation+Internal.h"
20
19
  #import "UIViewController+LayoutProtocol.h"
21
20
  #import <React/RCTConstants.h>
@@ -143,9 +142,7 @@ static BOOL ANNWatchingBundles;
143
142
  [oldScreens addObjectsFromArray:[ANNOverlayCoordinator takeAllForReload]];
144
143
  // 4. The bar components and the tabs' cells and accessories have no store to empty: each screen and each tabs node
145
144
  // keeps its own roots in its registry, so a new node never gets an old root by its id, and the old roots stop with
146
- // their old nodes after the hold. The tabs' preload windows hide at once; their views go with their tabs. The loop
147
- // updates are off already.
148
- [ANNTabsPreloadWindow hideAllAfterReload];
145
+ // their old nodes after the hold. The loop updates are off already.
149
146
  // 5. The native screens of the old screens are silent by their emitter. The modals and overlays waiting to show
150
147
  // left what waits for them in steps 2 and 3; the screens that wait to show in a split view's column leave it here,
151
148
  // without an answer.
@@ -36,7 +36,7 @@ typedef NS_ENUM(NSInteger, ANNRenderWaitBar) {
36
36
 
37
37
  /// `screens` gives the screens the wait is about: their bar components, the screens the limit marks ready, the screens
38
38
  /// of the unknown-content rule, and with `ANNRenderWaitContentEachScreen` the screens whose content it waits for, each
39
- /// by the readiness it needs (`ANNWaitedReadinessOf`: content, or "root mounted" without its bar components). It is
39
+ /// by the readiness it needs (`ANNWaitedReadinessOf`: content, or "unseen" content without its bar components). It is
40
40
  /// asked as the wait starts, as it takes its screens again (`retakeScreens:`) and as its bar phase starts. `node` is
41
41
  /// the node of `ANNRenderWaitContentNode`; nil otherwise. `limit` 0: no limit.
42
42
  - (instancetype)initWithContent:(ANNRenderWaitContent)content
@@ -49,10 +49,10 @@ typedef NS_ENUM(NSInteger, ANNRenderWaitBar) {
49
49
  - (instancetype)init NS_UNAVAILABLE;
50
50
  + (instancetype)new NS_UNAVAILABLE;
51
51
 
52
- /// The longest a waited screen of the "root mounted" kind (`ANNWaitedReadinessOf`, an unselected tab's screen) is
53
- /// waited for, from the start, also when the wait has no limit: a screen the user doesn't see never holds a command for
54
- /// long. 0: only the wait's own limit.
55
- @property(nonatomic) NSTimeInterval mountLimit;
52
+ /// The longest a waited screen of the "unseen" kind (`ANNWaitedReadinessOf`, an unselected tab's screen) is waited
53
+ /// for, from the start, also when the wait has no limit: a screen the user doesn't see never holds a command for long.
54
+ /// 0: only the wait's own limit.
55
+ @property(nonatomic) NSTimeInterval unseenLimit;
56
56
 
57
57
  /// The rule for content of unknown readiness: 1 s after the start, a screen whose rendered state stays unknown (no safe
58
58
  /// area view says it) counts as ready; a screen whose safe area view spoke waits for its signal. Off by default (a root