@apexnative/navigation 1.0.0-alpha.10 → 1.0.0-alpha.11

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0-alpha.11](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.10...v1.0.0-alpha.11) (2026-10-31)
4
+
5
+
6
+ ### Features
7
+
8
+ * a pair's primary can take the trailing side or the bottom of a split ([#73](https://github.com/apexnative/navigation/issues/73)) ([90ddcd3](https://github.com/apexnative/navigation/commit/90ddcd3c62fa5878cc98651ca7a2cfcdf4645197))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **ios:** a pair and its insets settle at once when iPhone Duo folds or unfolds ([#71](https://github.com/apexnative/navigation/issues/71)) ([ad87ce1](https://github.com/apexnative/navigation/commit/ad87ce1669b55dfb6ad65bedfb8e119f08abb04e))
14
+
3
15
  ## [1.0.0-alpha.10](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.9...v1.0.0-alpha.10) (2026-10-30)
4
16
 
5
17
 
@@ -11,7 +11,10 @@ data class ArrangementDefinition(
11
11
  val axes: Axes,
12
12
  /** `split`: the primary's share along the split, 0–1, where a fold doesn't decide. */
13
13
  val fraction: Float,
14
- /** `overlay`: the edge the primary takes when the pair goes side by side at a fold. */
14
+ /**
15
+ * The edge the primary takes. `split`: leading or top puts it first, trailing or bottom second; `overlay`: its edge
16
+ * when the pair goes side by side at a fold.
17
+ */
15
18
  val primaryEdge: Edge,
16
19
  /** What a window too small for a pair shows. */
17
20
  val compact: Compact,
@@ -36,8 +39,9 @@ data class ArrangementDefinition(
36
39
  @JvmStatic
37
40
  fun parse(data: JSONObject): ArrangementDefinition {
38
41
  val fraction = data.optDouble("fraction", DEFAULT_FRACTION).takeUnless { it.isNaN() } ?: DEFAULT_FRACTION
42
+ val style = if (data.optString("style") == "overlay") Style.OVERLAY else Style.SPLIT
39
43
  return ArrangementDefinition(
40
- style = if (data.optString("style") == "overlay") Style.OVERLAY else Style.SPLIT,
44
+ style = style,
41
45
  axes = when (data.optString("axes")) {
42
46
  "horizontal" -> Axes.HORIZONTAL
43
47
  "vertical" -> Axes.VERTICAL
@@ -48,7 +52,9 @@ data class ArrangementDefinition(
48
52
  "leading" -> Edge.LEADING
49
53
  "top" -> Edge.TOP
50
54
  "bottom" -> Edge.BOTTOM
51
- else -> Edge.TRAILING
55
+ "trailing" -> Edge.TRAILING
56
+ // The default of each style: the primary first in a split, at the trailing side of an overlay.
57
+ else -> if (style == Style.SPLIT) Edge.LEADING else Edge.TRAILING
52
58
  },
53
59
  compact = if (data.optString("compact") == "pair") Compact.PAIR else Compact.PRIMARY,
54
60
  )
@@ -45,8 +45,9 @@ internal fun WindowFolds.Fold.across(area: Rect): PairFold? {
45
45
  /**
46
46
  * Decides the frames of a pair. A window too small for a pair (`largeWindow` false: Material's compact width or
47
47
  * height) shows the primary alone with `compact: 'primary'`. Otherwise, and with `compact: 'pair'`: `split` lies side
48
- * by side when wider than tall, else one above the other, the primary at the leading side or on top with its
49
- * `fraction`; a fold across the pair puts the boundary on it, and the screens on its two sides, off its bounds.
48
+ * by side when wider than tall, else one above the other, the primary first (at the leading side or on top) or, with
49
+ * `primaryEdge` trailing or bottom, second, with its `fraction`; a fold across the pair puts the boundary on it, and
50
+ * the screens on its two sides, off its bounds.
50
51
  * `overlay`: both the whole size, the primary over the secondary; side by side at a fold that the device is partly
51
52
  * folded at, or that hides what is under it, the primary on `primaryEdge`. Where the axes don't allow the way the pair
52
53
  * would lie, only the primary shows (an overlay stays one over the other).
@@ -67,9 +68,9 @@ internal object ArrangementPlanner {
67
68
  val sideBySide = fold?.vertical ?: (width > height)
68
69
  when {
69
70
  !definition.allows(sideBySide) -> ArrangementPlan(Layout.SINGLE, whole, null)
70
- // The primary at the leading side, or on top.
71
- fold != null -> atFold(fold, width, height, primaryFirst = !fold.vertical || !rtl)
72
- else -> byFraction(definition.fraction, width, height, sideBySide, rtl)
71
+ fold != null ->
72
+ atFold(fold, width, height, primaryFirst(definition.primaryEdge, fold.vertical, rtl))
73
+ else -> byFraction(definition, width, height, sideBySide, rtl)
73
74
  }
74
75
  }
75
76
  Style.OVERLAY ->
@@ -92,27 +93,31 @@ internal object ArrangementPlanner {
92
93
  }
93
94
 
94
95
  private fun byFraction(
95
- fraction: Float,
96
+ definition: ArrangementDefinition,
96
97
  width: Int,
97
98
  height: Int,
98
99
  sideBySide: Boolean,
99
100
  rtl: Boolean,
100
101
  ): ArrangementPlan {
102
+ val first = primaryFirst(definition.primaryEdge, sideBySide, rtl)
101
103
  if (!sideBySide) {
102
- val share = (height * fraction).roundToInt()
103
- return ArrangementPlan(Layout.STACKED, Rect(0, 0, width, share), Rect(0, share, width, height))
104
+ val share = (height * definition.fraction).roundToInt()
105
+ val boundary = if (first) share else height - share
106
+ val top = Rect(0, 0, width, boundary)
107
+ val bottom = Rect(0, boundary, width, height)
108
+ return ArrangementPlan(Layout.STACKED, if (first) top else bottom, if (first) bottom else top)
104
109
  }
105
- val share = (width * fraction).roundToInt()
106
- // The primary at the leading side: the right one from right to left.
107
- val boundary = if (rtl) width - share else share
110
+ val share = (width * definition.fraction).roundToInt()
111
+ // The primary's part on the left or on the right: the leading side is the right one from right to left.
112
+ val boundary = if (first) share else width - share
108
113
  val left = Rect(0, 0, boundary, height)
109
114
  val right = Rect(boundary, 0, width, height)
110
- return ArrangementPlan(Layout.SIDE_BY_SIDE, if (rtl) right else left, if (rtl) left else right)
115
+ return ArrangementPlan(Layout.SIDE_BY_SIDE, if (first) left else right, if (first) right else left)
111
116
  }
112
117
 
113
- // Whether the primary takes the left part of a vertical fold, or the top part of a horizontal one. An edge of the
114
- // other axis counts as the nearest one: leading as top and top as leading, trailing as bottom and bottom as
115
- // trailing.
118
+ // Whether the primary takes the left part of a vertical fold (or of a pair side by side), or the top part of a
119
+ // horizontal one (or of a pair one above the other). An edge of the other axis counts as the nearest one: leading
120
+ // as top and top as leading, trailing as bottom and bottom as trailing.
116
121
  private fun primaryFirst(edge: Edge, vertical: Boolean, rtl: Boolean): Boolean {
117
122
  val first = edge == Edge.LEADING || edge == Edge.TOP
118
123
  return if (vertical) first != rtl else first
@@ -140,7 +140,7 @@ The type argument is the props of the pair, which the commands check as for any
140
140
 
141
141
  - **`axes`**: `'horizontal'` side by side only, `'vertical'` one above the other only, `'both'` by default. Where the window allows neither, only the primary shows.
142
142
  - **`fraction`**: the primary's share of a split, above 0 and below 1, half by default. At a fold the fold decides.
143
- - **`primaryEdge`**: where the primary goes when an overlay goes side by side, `'trailing'` by default.
143
+ - **`primaryEdge`**: the side the primary takes. In a split `'leading'`, the default, puts it first, `'trailing'` second; in an overlay that goes side by side, `'trailing'` by default.
144
144
  - **`compact`**: what a window that isn't large shows: `'primary'`, the default, or `'pair'`.
145
145
 
146
146
  A pair has two screens, never more: in a `defineArrangement`, the primary's own `arrangement` doesn't count. Every key: [`defineArrangement`](/api/arrangement/defineArrangement).
@@ -163,6 +163,22 @@ Where only the primary shows, the secondary is still there, out of the window, a
163
163
 
164
164
  With `compact: 'pair'` of `defineArrangement` the pair shows everywhere: on a phone held upright, one above the other.
165
165
 
166
+ ### Which screen shows alone, and on which side
167
+
168
+ The primary leads the pair: its bar is the pair's, and it is the screen that shows alone outside a large window. To have the other screen alone on a phone or on iPhone Duo folded, make that one the primary. `primaryEdge` then puts it on the side the layout asks for:
169
+
170
+ ```ts
171
+ // The list on the leading side, the details on
172
+ // the trailing one; folded, the details alone
173
+ ItemWithList: defineArrangement<{ id: string }>({
174
+ primary: 'Details',
175
+ secondary: 'List',
176
+ primaryEdge: 'trailing',
177
+ }),
178
+ ```
179
+
180
+ The bar stays the details' bar folded and open, so opening iPhone Duo changes nothing in it: the list comes in beside the details, which keep their place in the window. One setting serves both ways a split lies: `'trailing'` and `'bottom'` put the primary second, at the trailing side or at the bottom; `'leading'` and `'top'` put it first.
181
+
166
182
  ## Split and overlay
167
183
 
168
184
  `style` says how the two share the window:
@@ -813,6 +813,10 @@
813
813
  "heading": "What each device shows",
814
814
  "anchor": "devices"
815
815
  },
816
+ {
817
+ "heading": "Which screen shows alone, and on which side",
818
+ "anchor": "primary-and-sides"
819
+ },
816
820
  {
817
821
  "heading": "Split and overlay",
818
822
  "anchor": "style"
@@ -48,6 +48,14 @@ static NSDirectionalRectEdge ANNOppositeEdge(NSDirectionalRectEdge edge) {
48
48
  }
49
49
  }
50
50
 
51
+ // `primaryEdge` of a split: whether the primary goes second, at the trailing side or at the bottom. A split has no edge
52
+ // of its own: UIKit puts the view of its primary placement first along either axis (measured on iPhone Duo), so the
53
+ // edge decides which screen goes into that placement.
54
+ static BOOL ANNSecondaryFirst(NSDictionary *data) {
55
+ return ![@"overlay" isEqual:data[@"style"]] &&
56
+ ([@"trailing" isEqual:data[@"primaryEdge"]] || [@"bottom" isEqual:data[@"primaryEdge"]]);
57
+ }
58
+
51
59
  // Overlay view properties with the edge a view takes when the pair goes side by side.
52
60
  static UIOverlayArrangementViewProperties *ANNOverlayProperties(UIOverlayArrangement *overlay,
53
61
  NSDirectionalRectEdge edge)
@@ -95,8 +103,11 @@ static UIArrangement *ANNArrangementOfData(NSDictionary *data) API_AVAILABLE(ios
95
103
  height.preferred = dimension;
96
104
  primary.height = height;
97
105
  }
98
- [split setViewProperties:primary
99
- forPlacement:UIArrangementViewControllerViewPlacementPrimary];
106
+ // The share is the primary screen's, in whichever placement it is.
107
+ UIArrangementViewControllerViewPlacement placement =
108
+ ANNSecondaryFirst(data) ? UIArrangementViewControllerViewPlacementSecondary
109
+ : UIArrangementViewControllerViewPlacementPrimary;
110
+ [split setViewProperties:primary forPlacement:placement];
100
111
  }
101
112
  return split;
102
113
  }
@@ -109,6 +120,38 @@ static UIArrangement *ANNPrimaryAloneArrangement(void) API_AVAILABLE(ios(27.1))
109
120
  return split;
110
121
  }
111
122
 
123
+ // A size range from `minimum` to `maximum`, preferring `preferred`.
124
+ static UISplitArrangementDimensionRange *ANNDimensionRange(UISplitArrangementDimension *minimum,
125
+ UISplitArrangementDimension *preferred,
126
+ UISplitArrangementDimension *maximum)
127
+ API_AVAILABLE(ios(27.1)) {
128
+ UISplitArrangementDimensionRange *range = [UISplitArrangementDimensionRange new];
129
+ range.minimum = minimum;
130
+ range.preferred = preferred;
131
+ range.maximum = maximum;
132
+ return range;
133
+ }
134
+
135
+ // The primary alone in the secondary placement, with the primary placement empty. UIKit keeps the empty placement's
136
+ // share of a split (measured on iPhone Duo folded: the primary in the lower half, the upper one blank), so the empty
137
+ // placement gets no room and the primary all of it, along either axis, which no shape of the window leaves out.
138
+ static UIArrangement *ANNSecondPlacementAloneArrangement(void) API_AVAILABLE(ios(27.1)) {
139
+ UISplitArrangement *split = [UISplitArrangement splitArrangement];
140
+ split.axes = UIAxisHorizontal | UIAxisVertical;
141
+ UISplitArrangementDimension *none = [UISplitArrangementDimension absoluteDimension:0];
142
+ UISplitArrangementDimension *all = [UISplitArrangementDimension fractionalDimension:1];
143
+ UISplitArrangementViewProperties *empty = [split.defaultViewProperties copy];
144
+ empty.width = ANNDimensionRange(none, none, none);
145
+ empty.height = ANNDimensionRange(none, none, none);
146
+ [split setViewProperties:empty forPlacement:UIArrangementViewControllerViewPlacementPrimary];
147
+ UISplitArrangementViewProperties *primary = [split.defaultViewProperties copy];
148
+ primary.width = ANNDimensionRange(all, all, all);
149
+ primary.height = ANNDimensionRange(all, all, all);
150
+ [split setViewProperties:primary
151
+ forPlacement:UIArrangementViewControllerViewPlacementSecondary];
152
+ return split;
153
+ }
154
+
112
155
  // Room for a pair: both size classes regular (iPad, iPhone Duo unfolded). Nil without a size class yet: the view
113
156
  // controller is not in a window.
114
157
  static NSNumber *ANNIsLargeWindow(UITraitCollection *traits) {
@@ -183,9 +226,20 @@ static BOOL ANNIsShown(UIViewController *screen, UIArrangementViewState *state,
183
226
  // (`compact: 'primary'`).
184
227
  UIArrangement *_arrangement;
185
228
  BOOL _primaryAloneWhenSmall;
229
+ // `primaryEdge` of a split at the trailing side or the bottom: the secondary is in UIKit's primary placement, the
230
+ // primary in its secondary one. The primary alone keeps its placement and the secondary leaves its own, so the
231
+ // primary never changes its container at a fold. The axes the pair may use, for the primary alone then.
232
+ BOOL _secondaryFirst;
233
+ UIAxis _axes;
234
+ // The size a running size transition leads to, zero without one: the shape the pair decides its axes by.
235
+ CGSize _transitionSize;
186
236
  // What UIKit has now, once an arrangement went to it.
187
237
  BOOL _arranged;
188
238
  BOOL _primaryAlone;
239
+ // The scene's size classes the pair takes for a size transition, and which transition set them last: only that
240
+ // one's end takes them away.
241
+ BOOL _sceneSizeClasses;
242
+ NSUInteger _sceneSizeClassesTransition;
189
243
  }
190
244
 
191
245
  - (instancetype)initWithLayoutInfo:(ANNLayoutInfo *)layoutInfo
@@ -218,14 +272,16 @@ static BOOL ANNIsShown(UIViewController *screen, UIArrangementViewState *state,
218
272
  ((ANNComponentViewController *)screen).arrangement = self;
219
273
  }
220
274
  }
221
- [self setViewController:primary
275
+ _secondaryFirst = ANNSecondaryFirst(data);
276
+ _axes = ANNArrangementAxes(data[@"axes"]);
277
+ [self setViewController:_secondaryFirst ? secondary : primary
222
278
  forPlacement:UIArrangementViewControllerViewPlacementPrimary];
223
- [self setViewController:secondary
279
+ [self setViewController:_secondaryFirst ? primary : secondary
224
280
  forPlacement:UIArrangementViewControllerViewPlacementSecondary];
225
281
  _arrangement = ANNArrangementOfData(data);
226
282
  _primaryAloneWhenSmall = ![@"pair" isEqual:data[@"compact"]];
227
283
  // Before it is in a window: the window's size classes, which it will most likely have.
228
- [self arrangeForTraits:ANNUtils.mainWindow.traitCollection animated:NO];
284
+ [self arrangeForTraits:ANNUtils.mainWindow.traitCollection];
229
285
  if (!_arranged) {
230
286
  [self updateArrangement:_arrangement animated:NO];
231
287
  }
@@ -239,19 +295,33 @@ static BOOL ANNIsShown(UIViewController *screen, UIArrangementViewState *state,
239
295
  // `compact: 'primary'`: the pair shows only in a large window; in any other (an iPhone, iPhone Duo folded) the primary
240
296
  // shows alone, and the secondary waits out of the window until the window is large (iPhone Duo unfolded). With
241
297
  // `compact: 'pair'` UIKit lays the pair out everywhere.
242
- - (void)arrangeForTraits:(UITraitCollection *)traits animated:(BOOL)animated {
298
+ - (void)arrangeForTraits:(UITraitCollection *)traits {
243
299
  NSNumber *large = ANNIsLargeWindow(traits);
244
300
  if (!large) {
245
301
  return;
246
302
  }
247
- BOOL alone = _primaryAloneWhenSmall && !large.boolValue;
303
+ BOOL alone = (_primaryAloneWhenSmall && !large.boolValue) ||
304
+ (_secondaryFirst && ![self axesAllowSize:[self pairSize]]);
248
305
  if (_arranged && alone == _primaryAlone) {
249
306
  return;
250
307
  }
251
308
  BOOL changed = _arranged;
252
309
  _arranged = YES;
253
310
  _primaryAlone = alone;
254
- [self updateArrangement:alone ? ANNPrimaryAloneArrangement() : _arrangement animated:animated];
311
+ if (_secondaryFirst) {
312
+ // The secondary leaves the window with its placement, as a split with no axis takes it out otherwise.
313
+ UIViewController *first = alone ? nil : _secondary;
314
+ if ([self viewControllerForPlacement:UIArrangementViewControllerViewPlacementPrimary] !=
315
+ first) {
316
+ [self setViewController:first
317
+ forPlacement:UIArrangementViewControllerViewPlacementPrimary
318
+ animated:NO];
319
+ }
320
+ [self updateArrangement:alone ? ANNSecondPlacementAloneArrangement() : _arrangement
321
+ animated:NO];
322
+ } else {
323
+ [self updateArrangement:alone ? ANNPrimaryAloneArrangement() : _arrangement animated:NO];
324
+ }
255
325
  // A secondary that leaves the window renders no content: the pair waits for it no more.
256
326
  if (alone && _secondaryCounted) {
257
327
  _secondaryCounted = NO;
@@ -264,8 +334,103 @@ static BOOL ANNIsShown(UIViewController *screen, UIArrangementViewState *state,
264
334
  }
265
335
  }
266
336
 
337
+ // Where the axes leave the pair no room, UIKit shows its primary placement alone: with the secondary first that would
338
+ // be the secondary, so the pair takes it out itself, by the shape UIKit goes by (side by side in a wide window, one
339
+ // above the other in a tall one, measured on iPhone Duo).
340
+ - (BOOL)axesAllowSize:(CGSize)size {
341
+ if ((_axes & UIAxisHorizontal) && (_axes & UIAxisVertical)) {
342
+ return YES;
343
+ }
344
+ BOOL wide = size.width > size.height;
345
+ return (_axes & (wide ? UIAxisHorizontal : UIAxisVertical)) != 0;
346
+ }
347
+
348
+ // The pair's size: where a size transition leads, its own once laid out, the window's before.
349
+ - (CGSize)pairSize {
350
+ if (!CGSizeEqualToSize(_transitionSize, CGSizeZero)) {
351
+ return _transitionSize;
352
+ }
353
+ CGSize size = self.viewIfLoaded.bounds.size;
354
+ return size.width > 0 && size.height > 0 ? size : ANNUtils.mainWindow.bounds.size;
355
+ }
356
+
357
+ // New size classes come with a new window size, which leads the pair to its new place. UIKit's own animation of the
358
+ // arrangement would aim at a layout it gives only in between (on iPhone Duo, one above the other while the window
359
+ // still has its old size classes) and play on after the pair took its real one: the screens lagged by half a second.
267
360
  - (void)sizeClassesDidChange {
268
- [self arrangeForTraits:self.traitCollection animated:self.viewIfLoaded.window != nil];
361
+ [self arrangeForTraits:self.traitCollection];
362
+ }
363
+
364
+ // On iPhone Duo the scene has its new size classes as the size transition starts, and the pair gets them only after
365
+ // UIKit laid it out in the new size: the primary first filled the whole open window, then gave half of it to the
366
+ // secondary. For the transition the pair takes the scene's size classes, so it lies as it will from its first layout.
367
+ // Only where the pair has the window's size classes: a sheet or a split view's column has its own.
368
+ - (void)viewWillTransitionToSize:(CGSize)size
369
+ withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
370
+ [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
371
+ // A new shape may leave the axes no room, or give it back: the pair takes it before its first layout in the size.
372
+ if (_secondaryFirst) {
373
+ _transitionSize = size;
374
+ [self arrangeForTraits:self.traitCollection];
375
+ __weak ANNArrangementViewController *weakPair = self;
376
+ void (^sized)(id<UIViewControllerTransitionCoordinatorContext>) =
377
+ ^(id<UIViewControllerTransitionCoordinatorContext> context) {
378
+ ANNArrangementViewController *pair = weakPair;
379
+ if (pair && CGSizeEqualToSize(pair->_transitionSize, size)) {
380
+ pair->_transitionSize = CGSizeZero;
381
+ }
382
+ };
383
+ [coordinator animateAlongsideTransition:nil completion:sized];
384
+ }
385
+ UITraitCollection *scene = self.viewIfLoaded.window.windowScene.traitCollection;
386
+ NSNumber *large = ANNIsLargeWindow(self.traitCollection);
387
+ NSNumber *largeScene = scene ? ANNIsLargeWindow(scene) : nil;
388
+ if (!large || !largeScene || ![self hasWindowSizeClasses]) {
389
+ return;
390
+ }
391
+ // The size classes the pair has now are the scene's of an earlier transition, still running: this one keeps them
392
+ // until its own end.
393
+ if ([large isEqual:largeScene] && !_sceneSizeClasses) {
394
+ return;
395
+ }
396
+ _sceneSizeClasses = YES;
397
+ NSUInteger transition = ++_sceneSizeClassesTransition;
398
+ self.traitOverrides.horizontalSizeClass = scene.horizontalSizeClass;
399
+ self.traitOverrides.verticalSizeClass = scene.verticalSizeClass;
400
+ [self updateTraitsIfNeeded];
401
+ __weak ANNArrangementViewController *weakSelf = self;
402
+ void (^ended)(id<UIViewControllerTransitionCoordinatorContext>) =
403
+ ^(id<UIViewControllerTransitionCoordinatorContext> context) {
404
+ [weakSelf dropSceneSizeClassesOfTransition:transition];
405
+ };
406
+ [coordinator animateAlongsideTransition:nil completion:ended];
407
+ }
408
+
409
+ - (void)dropSceneSizeClassesOfTransition:(NSUInteger)transition {
410
+ if (!_sceneSizeClasses || transition != _sceneSizeClassesTransition) {
411
+ return;
412
+ }
413
+ _sceneSizeClasses = NO;
414
+ [self.traitOverrides removeTrait:UITraitHorizontalSizeClass.class];
415
+ [self.traitOverrides removeTrait:UITraitVerticalSizeClass.class];
416
+ }
417
+
418
+ // The pair's size classes are the window's: it stands in the window's root, not in a presented layer, and no split
419
+ // view's column is between them.
420
+ - (BOOL)hasWindowSizeClasses {
421
+ UIWindow *window = self.viewIfLoaded.window;
422
+ if (!window) {
423
+ return NO;
424
+ }
425
+ UIViewController *top = self;
426
+ for (UIViewController *parent = self.parentViewController; parent;
427
+ parent = parent.parentViewController) {
428
+ if ([parent isKindOfClass:UISplitViewController.class]) {
429
+ return NO;
430
+ }
431
+ top = parent;
432
+ }
433
+ return top == window.rootViewController;
269
434
  }
270
435
 
271
436
  // The first state, before the pair is laid out: as UIKit lays it out in the window's shape (measured on iPhone Duo),
@@ -433,13 +598,17 @@ static BOOL ANNIsShown(UIViewController *screen, UIArrangementViewState *state,
433
598
 
434
599
  - (void)viewDidLayoutSubviews {
435
600
  [super viewDidLayoutSubviews];
601
+ // A size of its own without a transition (a column of a split view): the axes may allow another shape now.
602
+ if (_secondaryFirst) {
603
+ [self arrangeForTraits:self.traitCollection];
604
+ }
436
605
  [self setNeedsStateUpdate];
437
606
  }
438
607
 
439
608
  // In the window now: its own size classes, which the window's stood in for.
440
609
  - (void)viewWillAppear:(BOOL)animated {
441
610
  [super viewWillAppear:animated];
442
- [self arrangeForTraits:self.traitCollection animated:NO];
611
+ [self arrangeForTraits:self.traitCollection];
443
612
  }
444
613
 
445
614
  - (void)viewDidAppear:(BOOL)animated {
@@ -479,10 +648,12 @@ static BOOL ANNIsShown(UIViewController *screen, UIArrangementViewState *state,
479
648
  if (!self.viewIfLoaded.window || _screensDestroyed) {
480
649
  return;
481
650
  }
651
+ // By the screens' placements: with `primaryEdge` at the trailing side or the bottom they are the other way round,
652
+ // and a secondary out of its placement has none.
482
653
  UIArrangementViewState *primaryState =
483
- [self stateForPlacement:UIArrangementViewControllerViewPlacementPrimary];
654
+ [self stateForPlacement:[self placementForViewController:_primary]];
484
655
  UIArrangementViewState *secondaryState =
485
- [self stateForPlacement:UIArrangementViewControllerViewPlacementSecondary];
656
+ [self stateForPlacement:[self placementForViewController:_secondary]];
486
657
  CGRect primaryFrame = ANNShownFrame(_primary.viewIfLoaded, self.view);
487
658
  CGRect secondaryFrame = ANNShownFrame(_secondary.viewIfLoaded, self.view);
488
659
  BOOL primaryShown = ANNIsShown(_primary, primaryState, primaryFrame);
@@ -498,7 +669,8 @@ static BOOL ANNIsShown(UIViewController *screen, UIArrangementViewState *state,
498
669
  CGRect overlap = CGRectIntersection(primaryFrame, secondaryFrame);
499
670
  if (!CGRectIsNull(overlap) && overlap.size.width > 1 && overlap.size.height > 1) {
500
671
  layout = @"overlay";
501
- // UIKit puts the primary over the secondary (zIndex 1 and 0).
672
+ // UIKit puts the primary over the secondary (zIndex 1 and 0); an overlay keeps the screens in their own
673
+ // placements.
502
674
  primaryFront = primaryState.zIndex >= secondaryState.zIndex;
503
675
  secondaryFront = !primaryFront;
504
676
  } else {
@@ -18,7 +18,9 @@ extern NSNotificationName const ANNScreenSettlingDidEndNotification;
18
18
  + (void)beginWithTransition:(id<UIViewControllerTransitionCoordinator>)coordinator;
19
19
 
20
20
  /// The vertical bar's edge changed: the size transition, when one follows, comes a little later. The hinge's moves
21
- /// begin the settling too (`ANNHingeDidMoveNotification`): UIKit may give a new safe area before either.
21
+ /// begin the settling too (`ANNHingeDidMoveNotification`): UIKit may give a new safe area before either. Once the size
22
+ /// transition ended the screens are on their new display: the hinge, turning on to its end, holds nothing until it
23
+ /// rests (a new state of the hinge still does), and the settling ends after a shorter quiet time.
22
24
  + (void)beginForTraitChange;
23
25
 
24
26
  /// Whether the screens hold their insets now.
@@ -8,6 +8,9 @@ NSNotificationName const ANNScreenSettlingDidEndNotification = @"ANNScreenSettli
8
8
  // How long the values of the screens stand still before the settling ends. On iPhone Duo UIKit's last step of a fold
9
9
  // comes some 30 ms after the size transition's end, and the size transition some 50 ms after the bar's new edge.
10
10
  static const CFTimeInterval ANNScreenSettlingQuietTime = 0.15;
11
+ // Once the size transition ended, UIKit's last steps come within some 30 ms: the screens are on the new display by
12
+ // then, and every held moment shows them with the old one's insets.
13
+ static const CFTimeInterval ANNScreenSettlingQuietTimeAfterTransition = 0.05;
11
14
  // The settling holds the insets no longer than this after its last start, whatever keeps changing.
12
15
  static const CFTimeInterval ANNScreenSettlingLongest = 1.0;
13
16
  // The degrees the hinge turns before it counts as a move: a device held half open trembles a little.
@@ -16,6 +19,11 @@ static const double ANNScreenSettlingHingeStep = 2;
16
19
  static BOOL ANNSettling;
17
20
  // The size transitions that began and haven't ended: each screen counts its own call of the same transition.
18
21
  static NSInteger ANNSettlingTransitions;
22
+ // A size transition of this settling ended: the hinge, still turning, no longer holds the insets.
23
+ static BOOL ANNSettlingTransitionEnded;
24
+ // Until when the hinge's turns after a size transition count for nothing: the hinge goes on turning to its end once
25
+ // the display switched, and holds nothing until it rests a while.
26
+ static CFTimeInterval ANNSettlingHingeIgnoredUntil;
19
27
  static CFTimeInterval ANNSettlingStart;
20
28
  static CFTimeInterval ANNSettlingLastChange;
21
29
  // Only the check scheduled last runs.
@@ -51,13 +59,22 @@ static NSNumber *ANNSettlingHingeAngle;
51
59
  return;
52
60
  }
53
61
  double turned = fabs(angle.doubleValue - ANNSettlingHingeAngle.doubleValue);
54
- BOOL moved = ![state isEqual:ANNSettlingHingeState] || !angle || !ANNSettlingHingeAngle ||
55
- turned >= ANNScreenSettlingHingeStep;
62
+ BOOL newState = ![state isEqual:ANNSettlingHingeState];
63
+ BOOL moved =
64
+ newState || !angle || !ANNSettlingHingeAngle || turned >= ANNScreenSettlingHingeStep;
56
65
  if (!moved) {
57
66
  return;
58
67
  }
59
68
  ANNSettlingHingeState = state;
60
69
  ANNSettlingHingeAngle = angle;
70
+ // A new state may bring a new layout; a turn after a size transition only goes on to the hinge's end.
71
+ CFTimeInterval now = CACurrentMediaTime();
72
+ BOOL afterTransition = (ANNSettling && ANNSettlingTransitionEnded) ||
73
+ now < ANNSettlingHingeIgnoredUntil;
74
+ if (!newState && afterTransition) {
75
+ ANNSettlingHingeIgnoredUntil = now + ANNScreenSettlingQuietTime;
76
+ return;
77
+ }
61
78
  [self beginForTraitChange];
62
79
  }
63
80
 
@@ -66,6 +83,7 @@ static NSNumber *ANNSettlingHingeAngle;
66
83
  return;
67
84
  }
68
85
  [self begin];
86
+ ANNSettlingTransitionEnded = NO;
69
87
  ANNSettlingTransitions += 1;
70
88
  NSUInteger epoch = ANNSettlingEpoch;
71
89
  void (^ended)(id<UIViewControllerTransitionCoordinatorContext>) =
@@ -74,8 +92,12 @@ static NSNumber *ANNSettlingHingeAngle;
74
92
  return;
75
93
  }
76
94
  ANNSettlingTransitions = MAX(0, ANNSettlingTransitions - 1);
95
+ if (ANNSettlingTransitions == 0) {
96
+ ANNSettlingTransitionEnded = YES;
97
+ ANNSettlingHingeIgnoredUntil = CACurrentMediaTime() + ANNScreenSettlingQuietTime;
98
+ }
77
99
  [ANNScreenSettling valuesDidChange];
78
- [ANNScreenSettling checkAfter:ANNScreenSettlingQuietTime];
100
+ [ANNScreenSettling checkAfter:[ANNScreenSettling quietTime]];
79
101
  };
80
102
  [coordinator animateAlongsideTransition:nil completion:ended];
81
103
  // The longest still holds should the transition's end never come.
@@ -84,10 +106,21 @@ static NSNumber *ANNSettlingHingeAngle;
84
106
 
85
107
  // Only iPhone Duo has a vertical bar: no need to ask for the hinge.
86
108
  + (void)beginForTraitChange {
109
+ // After the transition the bar's new edge is one of its last steps: it holds the values a little longer only.
110
+ if (ANNSettling && ANNSettlingTransitionEnded) {
111
+ [self valuesDidChange];
112
+ [self checkAfter:[self quietTime]];
113
+ return;
114
+ }
87
115
  [self begin];
88
116
  [self checkAfter:ANNScreenSettlingQuietTime];
89
117
  }
90
118
 
119
+ + (CFTimeInterval)quietTime {
120
+ return ANNSettlingTransitionEnded ? ANNScreenSettlingQuietTimeAfterTransition
121
+ : ANNScreenSettlingQuietTime;
122
+ }
123
+
91
124
  + (void)begin {
92
125
  CFTimeInterval now = CACurrentMediaTime();
93
126
  ANNSettling = YES;
@@ -121,10 +154,11 @@ static NSNumber *ANNSettlingHingeAngle;
121
154
  }
122
155
  CFTimeInterval now = CACurrentMediaTime();
123
156
  CFTimeInterval longest = ANNSettlingStart + ANNScreenSettlingLongest;
124
- CFTimeInterval quiet = ANNSettlingLastChange + ANNScreenSettlingQuietTime;
157
+ CFTimeInterval quiet = ANNSettlingLastChange + [self quietTime];
125
158
  if (now >= longest || (ANNSettlingTransitions == 0 && now >= quiet)) {
126
159
  ANNSettling = NO;
127
160
  ANNSettlingTransitions = 0;
161
+ ANNSettlingTransitionEnded = NO;
128
162
  ANNSettlingEpoch += 1;
129
163
  [NSNotificationCenter.defaultCenter postNotificationName:ANNScreenSettlingDidEndNotification
130
164
  object:nil];
@@ -44,14 +44,17 @@ export function isArrangement(value) {
44
44
  export function toEngineArrangement(primary, arrangement) {
45
45
  const full = arrangement;
46
46
  checkFraction(full.fraction);
47
+ const style = arrangement.style ?? 'split';
47
48
  return {
48
49
  primary,
49
50
  secondary: arrangement.secondary,
50
51
  secondaryProps: arrangement.secondaryProps,
51
- style: arrangement.style ?? 'split',
52
+ style,
52
53
  axes: full.axes ?? 'both',
53
54
  fraction: full.fraction ?? 0.5,
54
- primaryEdge: full.primaryEdge ?? 'trailing',
55
+ // Where UIKit puts the primary of each style without an edge set: first in a split, at the trailing side of an
56
+ // overlay that goes side by side.
57
+ primaryEdge: full.primaryEdge ?? (style === 'split' ? 'leading' : 'trailing'),
55
58
  compact: full.compact ?? 'primary'
56
59
  };
57
60
  }
@@ -1 +1 @@
1
- {"version":3,"names":["ARRANGEMENT","Symbol","defineArrangement","definition","checkFraction","fraction","isArrangement","value","toEngineArrangement","primary","arrangement","full","secondary","secondaryProps","style","axes","primaryEdge","compact","undefined","Error"],"sourceRoot":"../../../../src","sources":["api/screens/arrangement.ts"],"mappings":";;AAEA,MAAMA,WAAW,GAAGC,MAAM,CAAC,mCAAmC,CAAC;;AAE/D;AACA;AACA;AACA;AACA;;AAkBA;;AAkBA;;AAQA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAC/BC,UAAwC,EACpB;EACpBC,aAAa,CAACD,UAAU,CAACE,QAAQ,CAAC;EAClC,OAAO;IAAE,CAACL,WAAW,GAAG,IAAI;IAAEG;EAAW,CAAC;AAC5C;AAEA,OAAO,SAASG,aAAaA,CAACC,KAAc,EAA6B;EACvE,OAAO,OAAOA,KAAK,KAAK,QAAQ,IAAIA,KAAK,KAAK,IAAI,IAAIP,WAAW,IAAIO,KAAK;AAC5E;;AAEA;AACA,OAAO,SAASC,mBAAmBA,CACjCC,OAAe,EACfC,WAAgE,EAC7C;EACnB,MAAMC,IAAI,GAAGD,WAAyC;EACtDN,aAAa,CAACO,IAAI,CAACN,QAAQ,CAAC;EAC5B,OAAO;IACLI,OAAO;IACPG,SAAS,EAAEF,WAAW,CAACE,SAAS;IAChCC,cAAc,EAAEH,WAAW,CAACG,cAAc;IAC1CC,KAAK,EAAEJ,WAAW,CAACI,KAAK,IAAI,OAAO;IACnCC,IAAI,EAAEJ,IAAI,CAACI,IAAI,IAAI,MAAM;IACzBV,QAAQ,EAAEM,IAAI,CAACN,QAAQ,IAAI,GAAG;IAC9BW,WAAW,EAAEL,IAAI,CAACK,WAAW,IAAI,UAAU;IAC3CC,OAAO,EAAEN,IAAI,CAACM,OAAO,IAAI;EAC3B,CAAC;AACH;AAEA,SAASb,aAAaA,CAACC,QAA4B,EAAE;EACnD,IAAIA,QAAQ,KAAKa,SAAS,IAAI,EAAEb,QAAQ,GAAG,CAAC,IAAIA,QAAQ,GAAG,CAAC,CAAC,EAAE;IAC7D,MAAM,IAAIc,KAAK,CACb,yCAAyCd,QAAQ,+BACnD,CAAC;EACH;AACF","ignoreList":[]}
1
+ {"version":3,"names":["ARRANGEMENT","Symbol","defineArrangement","definition","checkFraction","fraction","isArrangement","value","toEngineArrangement","primary","arrangement","full","style","secondary","secondaryProps","axes","primaryEdge","compact","undefined","Error"],"sourceRoot":"../../../../src","sources":["api/screens/arrangement.ts"],"mappings":";;AAEA,MAAMA,WAAW,GAAGC,MAAM,CAAC,mCAAmC,CAAC;;AAE/D;AACA;AACA;AACA;AACA;;AAkBA;;AA4BA;;AAQA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAC/BC,UAAwC,EACpB;EACpBC,aAAa,CAACD,UAAU,CAACE,QAAQ,CAAC;EAClC,OAAO;IAAE,CAACL,WAAW,GAAG,IAAI;IAAEG;EAAW,CAAC;AAC5C;AAEA,OAAO,SAASG,aAAaA,CAACC,KAAc,EAA6B;EACvE,OAAO,OAAOA,KAAK,KAAK,QAAQ,IAAIA,KAAK,KAAK,IAAI,IAAIP,WAAW,IAAIO,KAAK;AAC5E;;AAEA;AACA,OAAO,SAASC,mBAAmBA,CACjCC,OAAe,EACfC,WAAgE,EAC7C;EACnB,MAAMC,IAAI,GAAGD,WAAyC;EACtDN,aAAa,CAACO,IAAI,CAACN,QAAQ,CAAC;EAC5B,MAAMO,KAAK,GAAGF,WAAW,CAACE,KAAK,IAAI,OAAO;EAC1C,OAAO;IACLH,OAAO;IACPI,SAAS,EAAEH,WAAW,CAACG,SAAS;IAChCC,cAAc,EAAEJ,WAAW,CAACI,cAAc;IAC1CF,KAAK;IACLG,IAAI,EAAEJ,IAAI,CAACI,IAAI,IAAI,MAAM;IACzBV,QAAQ,EAAEM,IAAI,CAACN,QAAQ,IAAI,GAAG;IAC9B;IACA;IACAW,WAAW,EAAEL,IAAI,CAACK,WAAW,KAAKJ,KAAK,KAAK,OAAO,GAAG,SAAS,GAAG,UAAU,CAAC;IAC7EK,OAAO,EAAEN,IAAI,CAACM,OAAO,IAAI;EAC3B,CAAC;AACH;AAEA,SAASb,aAAaA,CAACC,QAA4B,EAAE;EACnD,IAAIA,QAAQ,KAAKa,SAAS,IAAI,EAAEb,QAAQ,GAAG,CAAC,IAAIA,QAAQ,GAAG,CAAC,CAAC,EAAE;IAC7D,MAAM,IAAIc,KAAK,CACb,yCAAyCd,QAAQ,+BACnD,CAAC;EACH;AACF","ignoreList":[]}
@@ -23,13 +23,23 @@ export interface ScreenArrangement<Props extends object = {}> {
23
23
  }
24
24
  /** A pair under a name of its own, with every setting: `defineArrangement`. */
25
25
  export interface ArrangementDefinition<Props extends object = {}> extends ScreenArrangement<Props> {
26
- /** The screen that owns the bar: its title, buttons, toolbar and status bar are the pair's. A name of `screens`. */
26
+ /**
27
+ * The screen that owns the bar: its title, buttons, toolbar and status bar are the pair's. It shows alone outside a
28
+ * large window (`compact: 'primary'`), and `primaryEdge` puts it on either side. A name of `screens`.
29
+ */
27
30
  primary: string;
28
31
  /** The axes the pair may use. Where the window allows none of them, only the primary shows. @default 'both' */
29
32
  axes?: 'horizontal' | 'vertical' | 'both';
30
33
  /** `split`: the primary's share along the split, above 0 and below 1, where the fold does not decide. @default 0.5 */
31
34
  fraction?: number;
32
- /** `overlay`: the edge the primary takes when the pair goes side by side. @default 'trailing' */
35
+ /**
36
+ * The edge the primary takes. `split`: `leading` or `top` puts the primary first, at the leading side or on top;
37
+ * `trailing` or `bottom` puts it second, at the trailing side or at the bottom (one setting for both ways the pair
38
+ * may lie). `overlay`: the primary's edge when the pair goes side by side; an edge of the other axis counts as the
39
+ * nearest one (`top` as leading, `bottom` as trailing). Whatever the edge, the primary is the screen that shows
40
+ * alone outside a large window with `compact: 'primary'`: to show the other screen there, make it the primary.
41
+ * @default 'leading' for split, 'trailing' for overlay
42
+ */
33
43
  primaryEdge?: 'leading' | 'trailing' | 'top' | 'bottom';
34
44
  /**
35
45
  * Outside a large window: `primary` — the primary alone, as a plain screen; `pair` — the pair as the system lays it
@@ -1 +1 @@
1
- {"version":3,"file":"arrangement.d.ts","sourceRoot":"","sources":["../../../../src/api/screens/arrangement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAsB,CAAC;AAE9D,QAAA,MAAM,WAAW,eAA8C,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,iBAAiB,CAAC,KAAK,SAAS,MAAM,GAAG,EAAE;IAC1D;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IAC1C;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB,CAAC,KAAK,SAAS,MAAM,GAAG,EAAE,CAAE,SAAQ,iBAAiB,CAAC,KAAK,CAAC;IAChG,oHAAoH;IACpH,OAAO,EAAE,MAAM,CAAC;IAChB,+GAA+G;IAC/G,IAAI,CAAC,EAAE,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;IAC1C,sHAAsH;IACtH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,WAAW,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,CAAC;IACxD;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;CAC9B;AAED,yDAAyD;AACzD,MAAM,WAAW,WAAW,CAAC,KAAK,SAAS,MAAM,GAAG,EAAE;IACpD,QAAQ,CAAC,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAClD,gDAAgD;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,MAAM,GAAG,EAAE,EACzD,UAAU,EAAE,qBAAqB,CAAC,KAAK,CAAC,GACvC,WAAW,CAAC,KAAK,CAAC,CAGpB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,CAEvE;AAED,0GAA0G;AAC1G,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,iBAAiB,CAAC,GAAG,CAAC,GAAG,qBAAqB,CAAC,GAAG,CAAC,GAC/D,iBAAiB,CAanB"}
1
+ {"version":3,"file":"arrangement.d.ts","sourceRoot":"","sources":["../../../../src/api/screens/arrangement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAsB,CAAC;AAE9D,QAAA,MAAM,WAAW,eAA8C,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,iBAAiB,CAAC,KAAK,SAAS,MAAM,GAAG,EAAE;IAC1D;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IAC1C;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB,CAAC,KAAK,SAAS,MAAM,GAAG,EAAE,CAAE,SAAQ,iBAAiB,CAAC,KAAK,CAAC;IAChG;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,+GAA+G;IAC/G,IAAI,CAAC,EAAE,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;IAC1C,sHAAsH;IACtH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,CAAC;IACxD;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;CAC9B;AAED,yDAAyD;AACzD,MAAM,WAAW,WAAW,CAAC,KAAK,SAAS,MAAM,GAAG,EAAE;IACpD,QAAQ,CAAC,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAClD,gDAAgD;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,MAAM,GAAG,EAAE,EACzD,UAAU,EAAE,qBAAqB,CAAC,KAAK,CAAC,GACvC,WAAW,CAAC,KAAK,CAAC,CAGpB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,CAEvE;AAED,0GAA0G;AAC1G,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,iBAAiB,CAAC,GAAG,CAAC,GAAG,qBAAqB,CAAC,GAAG,CAAC,GAC/D,iBAAiB,CAgBnB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apexnative/navigation",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.11",
4
4
  "description": "Native navigation for React Native: platform stacks, tabs, modals and side menus on iOS, iPadOS and Android",
5
5
  "keywords": [
6
6
  "react-native",
@@ -26,13 +26,23 @@ export interface ScreenArrangement<Props extends object = {}> {
26
26
 
27
27
  /** A pair under a name of its own, with every setting: `defineArrangement`. */
28
28
  export interface ArrangementDefinition<Props extends object = {}> extends ScreenArrangement<Props> {
29
- /** The screen that owns the bar: its title, buttons, toolbar and status bar are the pair's. A name of `screens`. */
29
+ /**
30
+ * The screen that owns the bar: its title, buttons, toolbar and status bar are the pair's. It shows alone outside a
31
+ * large window (`compact: 'primary'`), and `primaryEdge` puts it on either side. A name of `screens`.
32
+ */
30
33
  primary: string;
31
34
  /** The axes the pair may use. Where the window allows none of them, only the primary shows. @default 'both' */
32
35
  axes?: 'horizontal' | 'vertical' | 'both';
33
36
  /** `split`: the primary's share along the split, above 0 and below 1, where the fold does not decide. @default 0.5 */
34
37
  fraction?: number;
35
- /** `overlay`: the edge the primary takes when the pair goes side by side. @default 'trailing' */
38
+ /**
39
+ * The edge the primary takes. `split`: `leading` or `top` puts the primary first, at the leading side or on top;
40
+ * `trailing` or `bottom` puts it second, at the trailing side or at the bottom (one setting for both ways the pair
41
+ * may lie). `overlay`: the primary's edge when the pair goes side by side; an edge of the other axis counts as the
42
+ * nearest one (`top` as leading, `bottom` as trailing). Whatever the edge, the primary is the screen that shows
43
+ * alone outside a large window with `compact: 'primary'`: to show the other screen there, make it the primary.
44
+ * @default 'leading' for split, 'trailing' for overlay
45
+ */
36
46
  primaryEdge?: 'leading' | 'trailing' | 'top' | 'bottom';
37
47
  /**
38
48
  * Outside a large window: `primary` — the primary alone, as a plain screen; `pair` — the pair as the system lays it
@@ -85,14 +95,17 @@ export function toEngineArrangement(
85
95
  ): EngineArrangement {
86
96
  const full = arrangement as ArrangementDefinition<any>;
87
97
  checkFraction(full.fraction);
98
+ const style = arrangement.style ?? 'split';
88
99
  return {
89
100
  primary,
90
101
  secondary: arrangement.secondary,
91
102
  secondaryProps: arrangement.secondaryProps,
92
- style: arrangement.style ?? 'split',
103
+ style,
93
104
  axes: full.axes ?? 'both',
94
105
  fraction: full.fraction ?? 0.5,
95
- primaryEdge: full.primaryEdge ?? 'trailing',
106
+ // Where UIKit puts the primary of each style without an edge set: first in a split, at the trailing side of an
107
+ // overlay that goes side by side.
108
+ primaryEdge: full.primaryEdge ?? (style === 'split' ? 'leading' : 'trailing'),
96
109
  compact: full.compact ?? 'primary',
97
110
  };
98
111
  }