@apexnative/navigation 1.0.0-alpha.3 → 1.0.0-alpha.5
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 +22 -0
- package/android/CLAUDE.md +26 -9
- package/android/CORE.md +32 -17
- package/android/RECIPES.md +9 -3
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +25 -9
- package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
- package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
- package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
- package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/AnimationsOptions.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/HardwareBackButtonOptions.kt +9 -1
- package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
- package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
- package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
- package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +6 -6
- package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +29 -36
- package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +33 -22
- package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
- package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +39 -20
- package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -4
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +6 -6
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +10 -5
- package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +47 -46
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
- package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
- package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +6 -4
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
- package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +3 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +25 -17
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +6 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenReadiness.kt +103 -56
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +8 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +8 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +61 -15
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +25 -12
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +2 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +85 -29
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/ScreenSearch.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +5 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +14 -11
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +56 -21
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
- package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
- package/cli/doctor/checks/app.js +61 -13
- package/cli/doctor/checks/splash.js +4 -22
- package/cli/migrate/native.js +18 -0
- package/cli/setup/source.js +2 -1
- package/cli/setup/steps/mainApplication.js +134 -19
- package/cli/splash/index.js +3 -1
- package/docs-md/architecture/transitions.md +6 -7
- package/docs-md/containers/modals.md +2 -2
- package/docs-md/guides/cli.md +1 -2
- package/docs-md/help/glossary.md +1 -1
- package/docs-md/help/troubleshooting.md +22 -5
- package/docs-md/index.json +4 -0
- package/docs-md/migration/from-react-native-navigation.md +8 -2
- package/docs-md/screen-ui/back-and-gestures.md +24 -2
- package/docs-md/start/installation.md +4 -2
- package/ios/CLAUDE.md +17 -5
- package/ios/CORE.md +18 -13
- package/ios/Contract/ANNCommandAnswer.h +9 -2
- package/ios/Contract/ANNCommandAnswer.mm +28 -30
- package/ios/Contract/ANNCommandContract.h +15 -0
- package/ios/Contract/ANNCommandContract.mm +78 -0
- package/ios/Core/ANNAppearanceDispatcher.h +5 -4
- package/ios/Core/ANNAppearanceDispatcher.mm +12 -1
- package/ios/Core/ANNErrorHandler.h +1 -1
- package/ios/Core/ANNErrorHandler.mm +3 -1
- package/ios/Core/ANNEventEmitter.h +1 -1
- package/ios/Core/ANNEventEmitter.mm +2 -2
- package/ios/Core/ANNJavaScriptCalls.h +34 -0
- package/ios/Core/ANNJavaScriptCalls.mm +61 -0
- package/ios/Core/ANNNavigationState.h +7 -5
- package/ios/Core/ANNNavigationState.mm +8 -7
- package/ios/Core/ANNNodeHelper.h +32 -12
- package/ios/Core/ANNNodeHelper.mm +100 -3
- package/ios/Core/ANNRenderWait.h +15 -1
- package/ios/Core/ANNRenderWait.mm +119 -17
- package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
- package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
- package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
- package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
- package/ios/Layouts/Root/ANNSplashViewController.h +5 -3
- package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
- package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
- package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
- package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
- package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
- package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
- package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
- package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
- package/ios/Layouts/Stack/ANNPreviewPush.h +5 -5
- package/ios/Layouts/Stack/ANNPreviewPush.mm +85 -22
- package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
- package/ios/Layouts/Stack/ANNStackController.mm +26 -3
- package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
- package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
- package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
- package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
- package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
- package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
- package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
- package/ios/Options/ANNOptionsCache.h +7 -6
- package/ios/Options/ANNSearchBarOptions.h +4 -3
- package/ios/Options/ANNUserChoice.h +57 -0
- package/ios/Options/ANNUserChoice.mm +44 -0
- package/ios/Options/Params/ANNImageSource.h +61 -0
- package/ios/Options/Params/ANNImageSource.mm +101 -0
- package/ios/Options/Params/ANNOptionImages.h +37 -0
- package/ios/Options/Params/ANNOptionImages.mm +284 -0
- package/ios/Options/Params/Image.h +11 -2
- package/ios/Options/Params/Image.mm +15 -2
- package/ios/Options/Params/ImageParser.h +5 -5
- package/ios/Options/Params/ImageParser.mm +28 -46
- package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
- package/ios/RECIPES.md +11 -5
- package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
- package/ios/React/Slots/ANNSlotContainers.h +14 -6
- package/ios/React/Slots/ANNSlotContainers.mm +75 -2
- package/ios/TopBar/ANNBarButtons.h +9 -5
- package/ios/TopBar/ANNBarButtons.mm +211 -63
- package/ios/TopBar/ANNIconButtonImage.h +1 -0
- package/ios/TopBar/ANNIconButtonImage.mm +34 -1
- package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
- package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
- package/ios/TopBar/ANNSearchPresenter.mm +170 -19
- package/ios/TopBar/ANNTopBarPresenter.h +5 -0
- package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
- package/ios/TurboModules/ANNTurboModule.mm +39 -21
- package/lib/module/ARCHITECTURE.md +18 -18
- package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
- package/lib/module/api/commands/commands.js +54 -6
- package/lib/module/api/commands/commands.js.map +1 -1
- package/lib/module/api/navigation.js.map +1 -1
- package/lib/module/engine/commands.js +9 -7
- package/lib/module/engine/commands.js.map +1 -1
- package/lib/module/engine/layout.js +13 -9
- package/lib/module/engine/layout.js.map +1 -1
- package/lib/module/engine/options.js +44 -11
- package/lib/module/engine/options.js.map +1 -1
- package/lib/module/engine/places.js +52 -7
- package/lib/module/engine/places.js.map +1 -1
- package/lib/module/engine/props.js +30 -1
- package/lib/module/engine/props.js.map +1 -1
- package/lib/module/engine/rememberedOptions.js +1 -1
- package/lib/module/engine/rememberedOptions.js.map +1 -1
- package/lib/module/engine/screens.js +22 -12
- package/lib/module/engine/screens.js.map +1 -1
- package/lib/module/interfaces/contract/commands.json +101 -0
- package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
- package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
- package/lib/typescript/api/commands/commands.d.ts.map +1 -1
- package/lib/typescript/api/commands/presentation.d.ts +4 -3
- package/lib/typescript/api/commands/presentation.d.ts.map +1 -1
- package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
- package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
- package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
- package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
- package/lib/typescript/api/navigation.d.ts +4 -1
- package/lib/typescript/api/navigation.d.ts.map +1 -1
- package/lib/typescript/engine/commands.d.ts.map +1 -1
- package/lib/typescript/engine/options.d.ts +5 -2
- package/lib/typescript/engine/options.d.ts.map +1 -1
- package/lib/typescript/engine/places.d.ts +7 -2
- package/lib/typescript/engine/places.d.ts.map +1 -1
- package/lib/typescript/engine/props.d.ts +8 -1
- package/lib/typescript/engine/props.d.ts.map +1 -1
- package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
- package/lib/typescript/engine/screens.d.ts.map +1 -1
- package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
- package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
- package/lib/typescript/interfaces/Events.d.ts +19 -10
- package/lib/typescript/interfaces/Events.d.ts.map +1 -1
- package/lib/typescript/interfaces/Options.d.ts +1 -1
- package/lib/typescript/interfaces/Options.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +23 -4
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
- package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/stack.d.ts +9 -0
- package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
- package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +18 -18
- package/src/adapters/NativeANNTurboModule.ts +5 -2
- package/src/api/commands/commands.ts +48 -7
- package/src/api/commands/presentation.ts +4 -3
- package/src/api/events/navigationEvents.ts +1 -1
- package/src/api/hooks/useNavigation.ts +1 -1
- package/src/api/navigation.ts +4 -1
- package/src/engine/commands.ts +19 -7
- package/src/engine/layout.ts +16 -9
- package/src/engine/options.ts +39 -11
- package/src/engine/places.ts +65 -7
- package/src/engine/props.ts +30 -1
- package/src/engine/rememberedOptions.ts +4 -1
- package/src/engine/screens.tsx +23 -13
- package/src/interfaces/ComponentEvents.ts +2 -2
- package/src/interfaces/Events.ts +19 -10
- package/src/interfaces/Options.ts +1 -0
- package/src/interfaces/contract/commands.json +101 -0
- package/src/interfaces/options/animations.ts +24 -4
- package/src/interfaces/options/buttons.ts +13 -5
- package/src/interfaces/options/stack.ts +9 -0
- package/src/interfaces/options/topBar.ts +22 -9
- package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
- package/android/src/main/java/dev/apexnative/navigation/react/events/StateHold.kt +0 -62
|
@@ -277,8 +277,8 @@ static void ANNCollectIds(UIViewController *node, NSMutableSet<NSString *> *ids)
|
|
|
277
277
|
|
|
278
278
|
// The end steps of a record (its show presented, its dismissal ended, closed by the user) run inside UIKit's
|
|
279
279
|
// completions and delegate calls, outside the runner's protection, in any state: a failure there is logged in one line,
|
|
280
|
-
//
|
|
281
|
-
// The gate's `done` ran before the step, as without a failure.
|
|
280
|
+
// answers what of the record still waits for an answer (its show, a chain step, its dismissals:
|
|
281
|
+
// `endFailedOf:showing:exception:`), and its drive stops. The gate's `done` ran before the step, as without a failure.
|
|
282
282
|
+ (void)protectEndOf:(ANNModal *)modal showing:(BOOL)showing step:(dispatch_block_t)step {
|
|
283
283
|
@try {
|
|
284
284
|
step();
|
|
@@ -288,24 +288,43 @@ static void ANNCollectIds(UIViewController *node, NSMutableSet<NSString *> *ids)
|
|
|
288
288
|
: (showing ? @"show a modal" : @"dismiss a modal");
|
|
289
289
|
NSString *reason = exception.reason.length > 0 ? exception.reason : exception.name;
|
|
290
290
|
RCTLogWarn(@"[ApexNavigation] %@", [ANNErrorTexts couldNot:what reason:reason]);
|
|
291
|
-
[self endFailedOf:modal exception:exception];
|
|
291
|
+
[self endFailedOf:modal showing:showing exception:exception];
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
|
|
295
|
+
// A failed end step of `modal`: its show rejects. After a failed show the dismissals that wait for it don't fail with
|
|
296
|
+
// it, as on Android: on a modal that shows they run now and resolve once it went (one still `Presenting` shows: UIKit's
|
|
297
|
+
// completion ran); on one that goes another way (a `dismissAllModals`, with the old root) or went already they resolve
|
|
298
|
+
// with its root's id; on one in another transition (a drag, its own dismissal) they wait for that transition's end.
|
|
299
|
+
// A failed dismissal rejects itself and the dismissals behind it.
|
|
300
|
+
+ (void)endFailedOf:(ANNModal *)modal showing:(BOOL)showing exception:(NSException *)exception {
|
|
296
301
|
if (!modal) {
|
|
297
302
|
return;
|
|
298
303
|
}
|
|
299
304
|
[self stopDriving:modal];
|
|
300
305
|
NSString *show = [NSString stringWithFormat:@"show the screen '%@'", modal.rootId];
|
|
301
|
-
NSString *dismiss = [NSString stringWithFormat:@"dismiss the modal '%@'", modal.rootId];
|
|
302
306
|
[modal.showAnswer reject:[ANNCommandRunner rejectionOf:exception what:show]];
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
ANNModalState state = modal.state;
|
|
308
|
+
BOOL goesAnotherWay = (state == ANNModalStateDismissing && modal.batch) ||
|
|
309
|
+
state == ANNModalStateWithOldRoot ||
|
|
310
|
+
state == ANNModalStateClosedBelow || state == ANNModalStateGone;
|
|
311
|
+
if (showing && state == ANNModalStatePresenting) {
|
|
312
|
+
modal.state = ANNModalStateShown;
|
|
313
|
+
}
|
|
314
|
+
if (showing && modal.state == ANNModalStateShown) {
|
|
315
|
+
[self runPendingOf:modal];
|
|
316
|
+
} else if (showing && goesAnotherWay) {
|
|
317
|
+
[modal.dismissal.answer resolve:modal.rootId];
|
|
318
|
+
[self resolvePendingOf:modal];
|
|
319
|
+
} else if (!showing) {
|
|
320
|
+
NSString *dismiss = [NSString stringWithFormat:@"dismiss the modal '%@'", modal.rootId];
|
|
321
|
+
NSString *dismissText = [ANNCommandRunner rejectionOf:exception what:dismiss];
|
|
322
|
+
[modal.dismissal.answer reject:dismissText];
|
|
323
|
+
NSArray<ANNModalDismissal *> *pending = modal.pending.copy;
|
|
324
|
+
[modal.pending removeAllObjects];
|
|
325
|
+
for (ANNModalDismissal *waiting in pending) {
|
|
326
|
+
[waiting.answer reject:dismissText];
|
|
327
|
+
}
|
|
309
328
|
}
|
|
310
329
|
// A `setRoot` chain goes on (its own steps are guarded by the root coordinator).
|
|
311
330
|
void (^chainStep)(BOOL) = modal.chainStep;
|
|
@@ -333,7 +352,7 @@ static void ANNCollectIds(UIViewController *node, NSMutableSet<NSString *> *ids)
|
|
|
333
352
|
[waiter reject:text];
|
|
334
353
|
}
|
|
335
354
|
for (ANNModal *modal in batch.records.copy) {
|
|
336
|
-
[self endFailedOf:modal exception:exception];
|
|
355
|
+
[self endFailedOf:modal showing:NO exception:exception];
|
|
337
356
|
}
|
|
338
357
|
}
|
|
339
358
|
}
|
|
@@ -537,7 +556,8 @@ static void ANNCollectIds(UIViewController *node, NSMutableSet<NSString *> *ids)
|
|
|
537
556
|
modal.coversUnder = under != nil && under.viewIfLoaded.window == nil;
|
|
538
557
|
NSArray<UIViewController *> *hidden = modal.coversUnder ? @[ under ] : @[];
|
|
539
558
|
@try {
|
|
540
|
-
// The chain step stays on the record until its events went: a failure there tells the chain
|
|
559
|
+
// The chain step stays on the record until its events went: a failure there tells the chain
|
|
560
|
+
// (`endFailedOf:showing:exception:`).
|
|
541
561
|
void (^chainStep)(BOOL) = modal.chainStep;
|
|
542
562
|
if (chainStep) {
|
|
543
563
|
[ANNAppearanceDispatcher endSwitchHiding:hidden showing:@[ root ] events:nil];
|
|
@@ -778,6 +798,11 @@ static void ANNCollectIds(UIViewController *node, NSMutableSet<NSString *> *ids)
|
|
|
778
798
|
[answer reject:[ANNErrorTexts screenNotShown:componentId]];
|
|
779
799
|
return;
|
|
780
800
|
}
|
|
801
|
+
// The last modal over no root (shown before any `setRoot`) stays: nothing would be left to show, as on Android.
|
|
802
|
+
if ([self isLastWithoutRoot:modal]) {
|
|
803
|
+
[answer reject:[ANNErrorTexts modalWithoutRoot:componentId]];
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
781
806
|
// Step 1: the options, read on the main queue; one that can't be read rejects before anything changes. They merge
|
|
782
807
|
// into the modal's visible screen as `mergeOptions` would.
|
|
783
808
|
if (options.count > 0) {
|
|
@@ -815,6 +840,22 @@ static void ANNCollectIds(UIViewController *node, NSMutableSet<NSString *> *ids)
|
|
|
815
840
|
[self dismiss:modal with:dismissal];
|
|
816
841
|
}
|
|
817
842
|
|
|
843
|
+
// No root in the window (the splash, or nothing yet) and no other modal that shows or comes: `modal` is all a dismissal
|
|
844
|
+
// would take away. One closed below, going with the old root or gone takes nothing away, so it doesn't count.
|
|
845
|
+
+ (BOOL)isLastWithoutRoot:(ANNModal *)modal {
|
|
846
|
+
if (modal.state == ANNModalStateClosedBelow || modal.state == ANNModalStateGone ||
|
|
847
|
+
ApexNavigation.mainWindow.rootViewController.isLayoutNode) {
|
|
848
|
+
return NO;
|
|
849
|
+
}
|
|
850
|
+
for (ANNModal *other in ANNModals) {
|
|
851
|
+
if (other != modal && other.state != ANNModalStateClosedBelow &&
|
|
852
|
+
other.state != ANNModalStateWithOldRoot && other.state != ANNModalStateGone) {
|
|
853
|
+
return NO;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return YES;
|
|
857
|
+
}
|
|
858
|
+
|
|
818
859
|
// The modal of `node`: the record whose root is the top of `node`'s layout.
|
|
819
860
|
+ (ANNModal *)modalOf:(UIViewController *)node {
|
|
820
861
|
UIViewController *top = node;
|
|
@@ -897,7 +938,7 @@ static void ANNCollectIds(UIViewController *node, NSMutableSet<NSString *> *ids)
|
|
|
897
938
|
[ANNErrorTexts couldNot:what reason:exception.reason ?: exception.name]);
|
|
898
939
|
// The dismissal still open answers, and so do the dismissals waiting behind it (an answer given already
|
|
899
940
|
// stays as it was).
|
|
900
|
-
[self endFailedOf:modal exception:exception];
|
|
941
|
+
[self endFailedOf:modal showing:NO exception:exception];
|
|
901
942
|
done();
|
|
902
943
|
}
|
|
903
944
|
}
|
|
@@ -7,9 +7,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
7
7
|
/// either at once. The status bar follows the Info.plist keys `UIStatusBarStyle` and `UIStatusBarHidden`. Main queue.
|
|
8
8
|
@interface ANNSplashViewController : UIViewController
|
|
9
9
|
|
|
10
|
-
/// The copy of the launch screen
|
|
11
|
-
///
|
|
12
|
-
///
|
|
10
|
+
/// The copy of the launch screen. When iOS builds it from the Info.plist (`UILaunchScreen`, or the default of
|
|
11
|
+
/// `UILaunchScreens`), the same color, image and bars as plain views; else the initial view controller of
|
|
12
|
+
/// `UILaunchStoryboardName` as a child (a storyboard that doesn't load: the first view of the nib of that name), else
|
|
13
|
+
/// the legacy launch images, full screen. Nil when the app has none of these: the window then has no root view
|
|
14
|
+
/// controller until the first root.
|
|
13
15
|
+ (nullable instancetype)launchScreenCopyWithBounds:(CGRect)bounds;
|
|
14
16
|
|
|
15
17
|
/// The blank screen after a reload: clear when the default options give the window a color (`window.backgroundColor`),
|
|
@@ -34,9 +34,63 @@ static NSArray<NSString *> *ANNLegacyLaunchImageNames(CGFloat height) {
|
|
|
34
34
|
return names;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
static NSDictionary *ANNDictionaryOrNil(id value) {
|
|
38
|
+
return [value isKindOfClass:NSDictionary.class] ? value : nil;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static NSString *ANNStringOrNil(id value) {
|
|
42
|
+
return [value isKindOfClass:NSString.class] && [value length] > 0 ? value : nil;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The definition that a plural key (`UILaunchScreens`, `UILaunchStoryboards`) marks as its default.
|
|
46
|
+
static NSDictionary *ANNDefaultDefinition(id plural, NSString *definitionsKey,
|
|
47
|
+
NSString *identifierKey, NSString *defaultKey) {
|
|
48
|
+
NSDictionary *dictionary = ANNDictionaryOrNil(plural);
|
|
49
|
+
NSArray *definitions = dictionary[definitionsKey];
|
|
50
|
+
NSString *defaultIdentifier = ANNStringOrNil(dictionary[defaultKey]);
|
|
51
|
+
if (![definitions isKindOfClass:NSArray.class] || !defaultIdentifier) {
|
|
52
|
+
return nil;
|
|
53
|
+
}
|
|
54
|
+
for (id definition in definitions) {
|
|
55
|
+
NSDictionary *entry = ANNDictionaryOrNil(definition);
|
|
56
|
+
if ([entry[identifierKey] isEqual:defaultIdentifier]) {
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return nil;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The launch screen that iOS builds from the Info.plist without a storyboard, or nil when it shows a storyboard. Of
|
|
64
|
+
// `UILaunchStoryboardName`, `UILaunchScreen`, the default of `UILaunchStoryboards` and the default of
|
|
65
|
+
// `UILaunchScreens`, the last one present wins (SplashBoard's `XBApplicationLaunchCompatibilityInfo`). A launch by a
|
|
66
|
+
// link that `UILaunchScreens` maps to a screen of its own still gets the default here.
|
|
67
|
+
static NSDictionary *ANNLaunchScreenConfiguration(NSBundle *bundle) {
|
|
68
|
+
NSDictionary *screen =
|
|
69
|
+
ANNDefaultDefinition([bundle objectForInfoDictionaryKey:@"UILaunchScreens"],
|
|
70
|
+
@"UILaunchScreenDefinitions", @"UILaunchScreenIdentifier",
|
|
71
|
+
@"UIDefaultLaunchScreen");
|
|
72
|
+
if (screen) {
|
|
73
|
+
return screen;
|
|
74
|
+
}
|
|
75
|
+
if (ANNDefaultDefinition([bundle objectForInfoDictionaryKey:@"UILaunchStoryboards"],
|
|
76
|
+
@"UILaunchStoryboardDefinitions", @"UILaunchStoryboardIdentifier",
|
|
77
|
+
@"UIDefaultLaunchStoryboard")) {
|
|
78
|
+
return nil;
|
|
79
|
+
}
|
|
80
|
+
return ANNDictionaryOrNil([bundle objectForInfoDictionaryKey:@"UILaunchScreen"]);
|
|
81
|
+
}
|
|
82
|
+
|
|
37
83
|
@implementation ANNSplashViewController {
|
|
38
84
|
UIViewController *_content;
|
|
39
85
|
UIView *_contentView;
|
|
86
|
+
NSDictionary *_launchScreen;
|
|
87
|
+
// The parts of a launch screen built from `UILaunchScreen`, laid out once the safe area is known.
|
|
88
|
+
UIImageView *_imageView;
|
|
89
|
+
BOOL _imageRespectsSafeArea;
|
|
90
|
+
UINavigationBar *_navigationBar;
|
|
91
|
+
UIImageView *_navigationBarImageView;
|
|
92
|
+
UIView *_bottomBar;
|
|
93
|
+
UIImageView *_bottomBarImageView;
|
|
40
94
|
}
|
|
41
95
|
|
|
42
96
|
- (instancetype)initWithContent:(UIViewController *)content
|
|
@@ -52,6 +106,12 @@ static NSArray<NSString *> *ANNLegacyLaunchImageNames(CGFloat height) {
|
|
|
52
106
|
|
|
53
107
|
+ (instancetype)launchScreenCopyWithBounds:(CGRect)bounds {
|
|
54
108
|
NSBundle *bundle = NSBundle.mainBundle;
|
|
109
|
+
NSDictionary *launchScreen = ANNLaunchScreenConfiguration(bundle);
|
|
110
|
+
if (launchScreen) {
|
|
111
|
+
ANNSplashViewController *copy = [[self alloc] initWithContent:nil contentView:nil blank:NO];
|
|
112
|
+
copy->_launchScreen = launchScreen;
|
|
113
|
+
return copy;
|
|
114
|
+
}
|
|
55
115
|
NSString *storyboardName = [bundle objectForInfoDictionaryKey:@"UILaunchStoryboardName"];
|
|
56
116
|
if ([storyboardName isKindOfClass:NSString.class] && storyboardName.length > 0) {
|
|
57
117
|
UIViewController *initial = nil;
|
|
@@ -97,6 +157,10 @@ static NSArray<NSString *> *ANNLegacyLaunchImageNames(CGFloat height) {
|
|
|
97
157
|
view.backgroundColor = windowHasColor ? UIColor.clearColor : UIColor.systemBackgroundColor;
|
|
98
158
|
return;
|
|
99
159
|
}
|
|
160
|
+
if (_launchScreen) {
|
|
161
|
+
[self buildLaunchScreen];
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
100
164
|
UIView *contentView = _contentView;
|
|
101
165
|
if (_content) {
|
|
102
166
|
[self addChildViewController:_content];
|
|
@@ -109,6 +173,89 @@ static NSArray<NSString *> *ANNLegacyLaunchImageNames(CGFloat height) {
|
|
|
109
173
|
[_content didMoveToParentViewController:self];
|
|
110
174
|
}
|
|
111
175
|
|
|
176
|
+
#pragma mark - The launch screen of `UILaunchScreen`
|
|
177
|
+
|
|
178
|
+
// Built as SplashBoard builds it: the named color over the whole screen, else the system background; the named image
|
|
179
|
+
// at its own size in the middle of the screen or of its safe area, stretched instead when the asset has slicing; a tab
|
|
180
|
+
// bar or a toolbar at the bottom and a navigation bar at the top, the system's own bars, or the bar's named image
|
|
181
|
+
// stretched over the bar's place. A name the asset catalog doesn't have leaves its part out, as iOS does.
|
|
182
|
+
- (void)buildLaunchScreen {
|
|
183
|
+
UIView *view = self.view;
|
|
184
|
+
NSString *colorName = ANNStringOrNil(_launchScreen[@"UIColorName"]);
|
|
185
|
+
UIColor *color = colorName ? [UIColor colorNamed:colorName] : nil;
|
|
186
|
+
view.backgroundColor = color ?: UIColor.systemBackgroundColor;
|
|
187
|
+
|
|
188
|
+
NSString *imageName = ANNStringOrNil(_launchScreen[@"UIImageName"]);
|
|
189
|
+
UIImage *image = imageName ? [UIImage imageNamed:imageName] : nil;
|
|
190
|
+
if (image) {
|
|
191
|
+
_imageView = [[UIImageView alloc] initWithImage:image];
|
|
192
|
+
_imageView.contentMode = UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero)
|
|
193
|
+
? UIViewContentModeCenter
|
|
194
|
+
: UIViewContentModeScaleToFill;
|
|
195
|
+
id respects = _launchScreen[@"UIImageRespectsSafeAreaInsets"];
|
|
196
|
+
_imageRespectsSafeArea = [respects respondsToSelector:@selector(boolValue)] &&
|
|
197
|
+
[respects boolValue];
|
|
198
|
+
[view addSubview:_imageView];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// A toolbar takes the bottom before a tab bar; a navigation bar comes on top of either.
|
|
202
|
+
id toolbar = _launchScreen[@"UIToolbar"];
|
|
203
|
+
id tabBar = _launchScreen[@"UITabBar"];
|
|
204
|
+
if (toolbar || tabBar) {
|
|
205
|
+
_bottomBar = toolbar ? [UIToolbar new] : [UITabBar new];
|
|
206
|
+
_bottomBarImageView = [self addBar:_bottomBar configuration:toolbar ?: tabBar];
|
|
207
|
+
}
|
|
208
|
+
id navigationBar = _launchScreen[@"UINavigationBar"];
|
|
209
|
+
if (navigationBar) {
|
|
210
|
+
_navigationBar = [UINavigationBar new];
|
|
211
|
+
_navigationBarImageView = [self addBar:_navigationBar configuration:navigationBar];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Adds the bar, or its image in its place when its configuration names one that loads.
|
|
216
|
+
- (UIImageView *)addBar:(UIView *)bar configuration:(id)configuration {
|
|
217
|
+
NSString *name = ANNStringOrNil(ANNDictionaryOrNil(configuration)[@"UIImageName"]);
|
|
218
|
+
UIImage *image = name ? [UIImage imageNamed:name] : nil;
|
|
219
|
+
UIImageView *imageView = image ? [[UIImageView alloc] initWithImage:image] : nil;
|
|
220
|
+
bar.hidden = imageView != nil;
|
|
221
|
+
[self.view addSubview:bar];
|
|
222
|
+
if (imageView) {
|
|
223
|
+
[self.view addSubview:imageView];
|
|
224
|
+
}
|
|
225
|
+
return imageView;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The safe area is known only in the window, so the parts are laid out here, after every change of it.
|
|
229
|
+
- (void)viewDidLayoutSubviews {
|
|
230
|
+
[super viewDidLayoutSubviews];
|
|
231
|
+
if (!_launchScreen) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
CGRect bounds = self.view.bounds;
|
|
235
|
+
UIEdgeInsets safeArea = self.view.safeAreaInsets;
|
|
236
|
+
_imageView.frame =
|
|
237
|
+
_imageRespectsSafeArea ? UIEdgeInsetsInsetRect(bounds, safeArea) : bounds;
|
|
238
|
+
[self layOutBar:_bottomBar imageView:_bottomBarImageView atBottom:YES];
|
|
239
|
+
[self layOutBar:_navigationBar imageView:_navigationBarImageView atBottom:NO];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// A bar is as tall as UIKit makes it, plus the safe area on its edge of the screen, and as wide as the screen.
|
|
243
|
+
- (void)layOutBar:(UIView *)bar
|
|
244
|
+
imageView:(UIImageView *)imageView
|
|
245
|
+
atBottom:(BOOL)atBottom {
|
|
246
|
+
if (!bar) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
CGRect bounds = self.view.bounds;
|
|
250
|
+
UIEdgeInsets safeArea = self.view.safeAreaInsets;
|
|
251
|
+
CGFloat height = [bar sizeThatFits:bounds.size].height +
|
|
252
|
+
(atBottom ? safeArea.bottom : safeArea.top);
|
|
253
|
+
CGFloat y = atBottom ? CGRectGetMaxY(bounds) - height : CGRectGetMinY(bounds);
|
|
254
|
+
CGRect frame = CGRectMake(CGRectGetMinX(bounds), y, CGRectGetWidth(bounds), height);
|
|
255
|
+
bar.frame = frame;
|
|
256
|
+
imageView.frame = frame;
|
|
257
|
+
}
|
|
258
|
+
|
|
112
259
|
#pragma mark - The status bar, as the launch screen had it
|
|
113
260
|
|
|
114
261
|
- (UIStatusBarStyle)preferredStatusBarStyle {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#import "ANNArrangementScreenDelegate.h"
|
|
2
2
|
#import "ANNLayoutProtocol.h"
|
|
3
3
|
#import "ANNScreenAppearance.h"
|
|
4
|
+
#import "ANNScreenPlace.h"
|
|
4
5
|
#import "UIViewController+LayoutProtocol.h"
|
|
5
6
|
#import <UIKit/UIKit.h>
|
|
6
7
|
|
|
@@ -42,6 +43,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
42
43
|
/// nobody rendered gets its content when UIKit first loads its view.
|
|
43
44
|
- (void)render;
|
|
44
45
|
|
|
46
|
+
/// Where the screen will show while it waits for its content out of it (parked by a stack's or `showInColumn`'s wait,
|
|
47
|
+
/// `ANNParking`): it lays out with that place's safe area and bars (`ANNScreenPlace`), as it will once it shows there.
|
|
48
|
+
/// Asked at each layout, and kept past the parking until UIKit gives the screen a safe area of its own in a parent
|
|
49
|
+
/// (the push lays it out once before that, with none), or as it appears. Nil: its view's own. Set by the one that
|
|
50
|
+
/// parks the screen.
|
|
51
|
+
@property(nonatomic, copy, nullable) ANNScreenPlace *_Nullable (^placeWhileParked)(void);
|
|
52
|
+
|
|
45
53
|
/// Applies the "under bars" rule to the root frame again, then gives the modules the insets, the padding and the
|
|
46
54
|
/// regions of the screen. Nothing before the view loaded; the safe area only in a window.
|
|
47
55
|
- (void)updateRootLayout;
|
|
@@ -162,6 +162,11 @@
|
|
|
162
162
|
[ANNSystemUI setNeedsUpdate];
|
|
163
163
|
[self.arrangement screenDidChangeLayout:self];
|
|
164
164
|
[ANNHinge watchForScreens];
|
|
165
|
+
// A parked screen whose place gave it no safe area at all lays out with its view's own from now on.
|
|
166
|
+
if (self.placeWhileParked && ![ANNParking isUnseen:self]) {
|
|
167
|
+
self.placeWhileParked = nil;
|
|
168
|
+
[self updateRootLayout];
|
|
169
|
+
}
|
|
165
170
|
// Once JavaScript heard the appearance (a driven one ends later, with its command).
|
|
166
171
|
__weak ANNComponentViewController *weakSelf = self;
|
|
167
172
|
[ANNAppearanceDispatcher afterDidAppearOf:self.appearance
|
|
@@ -363,11 +368,31 @@
|
|
|
363
368
|
}
|
|
364
369
|
#endif
|
|
365
370
|
|
|
371
|
+
// The place the screen waits to show in, while it is parked out of it and until UIKit gives it a safe area of its
|
|
372
|
+
// own there: the push lays the screen out once before that, with none, and a list laid out then at the screen's top
|
|
373
|
+
// would stay the list the bar follows. A place that shows with no safe area at all hands over as the screen appears.
|
|
374
|
+
- (nullable ANNScreenPlace *)parkedPlace {
|
|
375
|
+
ANNScreenPlace * (^place)(void) = self.placeWhileParked;
|
|
376
|
+
if (!place) {
|
|
377
|
+
return nil;
|
|
378
|
+
}
|
|
379
|
+
if (![ANNParking isUnseen:self] && self.parentViewController &&
|
|
380
|
+
!UIEdgeInsetsEqualToEdgeInsets(self.view.safeAreaInsets, UIEdgeInsetsZero)) {
|
|
381
|
+
self.placeWhileParked = nil;
|
|
382
|
+
return nil;
|
|
383
|
+
}
|
|
384
|
+
return place();
|
|
385
|
+
}
|
|
386
|
+
|
|
366
387
|
- (CGRect)rootFrameForOptions:(ANNNavigationOptions *)options {
|
|
367
388
|
UIView *view = self.view;
|
|
368
|
-
|
|
369
|
-
BOOL
|
|
370
|
-
|
|
389
|
+
ANNScreenPlace *place = [self parkedPlace];
|
|
390
|
+
BOOL topBarShown = place ? place.showsTopBar : [self hasShownTopBar];
|
|
391
|
+
BOOL tabBarShown = place ? place.showsTabBar : [self hasShownTabBar];
|
|
392
|
+
BOOL fromTop = ANNScreenIsUnderTopBar(options) || !topBarShown;
|
|
393
|
+
BOOL toBottom = ANNScreenIsUnderTabBar(options) || !tabBarShown;
|
|
394
|
+
UIEdgeInsets safeArea = place ? place.safeArea : view.safeAreaInsets;
|
|
395
|
+
return ANNScreenRootFrame(view.bounds, safeArea, fromTop, toBottom);
|
|
371
396
|
}
|
|
372
397
|
|
|
373
398
|
- (void)updateRootLayout {
|
|
@@ -421,8 +446,10 @@
|
|
|
421
446
|
self.nodeHelper.destroyed) {
|
|
422
447
|
return;
|
|
423
448
|
}
|
|
424
|
-
|
|
425
|
-
|
|
449
|
+
// A parked screen has no safe area of its own: it takes its place's.
|
|
450
|
+
ANNScreenPlace *place = [self parkedPlace];
|
|
451
|
+
UIEdgeInsets safeArea = place ? place.safeArea : view.safeAreaInsets;
|
|
452
|
+
UIEdgeInsets insets = ANNScreenInsetsOverRoot(content.frame, view.bounds.size, safeArea);
|
|
426
453
|
UINavigationController *stack = [self shownNavigation];
|
|
427
454
|
ANNScreenPaddingContext context = {
|
|
428
455
|
.inStack = stack != nil,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#import "ANNNavigationOptions.h"
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
/// Where an entry will show on top of a stack, as UIKit lays it out there: the safe area its screens get, and whether
|
|
7
|
+
/// the stack's bar and the tab bar show for it. A screen that waits for its content out of its stack (parked,
|
|
8
|
+
/// `ANNParking`) has no parent view controller, and UIKit gives its view no safe area at all: it lays out with its
|
|
9
|
+
/// place's instead (`placeWhileParked` of `ANNComponentViewController`), so that its root frame, its padding, the
|
|
10
|
+
/// insets JavaScript reads in its first render and its main list are those it has after the push. Nothing of it moves
|
|
11
|
+
/// as the push starts: a list that moved then would lose the large title, which UIKit draws in the list the bar
|
|
12
|
+
/// follows.
|
|
13
|
+
///
|
|
14
|
+
/// The bar's height is UIKit's own: a navigation controller of the core's, unseen in the stack's view for the moment of
|
|
15
|
+
/// the measure, shows an item made like the entry's (title, display mode of the large title, looks, subtitles, prompt,
|
|
16
|
+
/// style, search field) over one like the stack's top, with the stack's bar settings and the entry's toolbar. Main
|
|
17
|
+
/// queue.
|
|
18
|
+
@interface ANNScreenPlace : NSObject
|
|
19
|
+
|
|
20
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
21
|
+
+ (instancetype)new NS_UNAVAILABLE;
|
|
22
|
+
|
|
23
|
+
/// The place of `entry` (a screen, or a pair whose screens show under its bar) on top of `stack`, with `options` the
|
|
24
|
+
/// resolved options the stack shows its bar and toolbar by; nil when the stack isn't in a window. Measured again only
|
|
25
|
+
/// when something the measure reads changed since the last one for `entry`.
|
|
26
|
+
+ (nullable ANNScreenPlace *)placeOfEntry:(UIViewController *)entry
|
|
27
|
+
inStack:(UINavigationController *)stack
|
|
28
|
+
options:(ANNNavigationOptions *)options;
|
|
29
|
+
|
|
30
|
+
/// The safe area of the entry's view in the place.
|
|
31
|
+
@property(nonatomic, readonly) UIEdgeInsets safeArea;
|
|
32
|
+
|
|
33
|
+
/// The stack's bar shows for the entry (`topBar.visible`).
|
|
34
|
+
@property(nonatomic, readonly) BOOL showsTopBar;
|
|
35
|
+
|
|
36
|
+
/// A tab bar shows at the bottom for the entry: one shows now and the entry doesn't hide it.
|
|
37
|
+
@property(nonatomic, readonly) BOOL showsTabBar;
|
|
38
|
+
|
|
39
|
+
@end
|
|
40
|
+
|
|
41
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#import "ANNScreenPlace.h"
|
|
2
|
+
#import "ANNAssert.h"
|
|
3
|
+
#import "ANNBottomTabsController.h"
|
|
4
|
+
#import <objc/runtime.h>
|
|
5
|
+
|
|
6
|
+
static const void *ANNScreenPlaceKey = &ANNScreenPlaceKey;
|
|
7
|
+
|
|
8
|
+
// The measuring navigation controller of each stack: its first item stands for the stack's top, the second for the
|
|
9
|
+
// entry. Weak keys: it goes with its stack.
|
|
10
|
+
static NSMapTable<UINavigationController *, UINavigationController *> *ANNPlaceProbes(void) {
|
|
11
|
+
static NSMapTable<UINavigationController *, UINavigationController *> *probes;
|
|
12
|
+
if (!probes) {
|
|
13
|
+
probes = [NSMapTable weakToStrongObjectsMapTable];
|
|
14
|
+
}
|
|
15
|
+
return probes;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static UINavigationController *ANNPlaceProbeOf(UINavigationController *stack) {
|
|
19
|
+
UINavigationController *probe = [ANNPlaceProbes() objectForKey:stack];
|
|
20
|
+
if (!probe) {
|
|
21
|
+
probe = [[UINavigationController alloc] initWithRootViewController:[UIViewController new]];
|
|
22
|
+
[probe pushViewController:[UIViewController new] animated:NO];
|
|
23
|
+
[ANNPlaceProbes() setObject:probe forKey:stack];
|
|
24
|
+
}
|
|
25
|
+
return probe;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// What UIKit sizes the bar by, from the entry's item. The search field is a field of its own with the same placement:
|
|
29
|
+
// a search controller shows in one item only.
|
|
30
|
+
static void ANNCopyBarItem(UINavigationItem *from, UINavigationItem *to) {
|
|
31
|
+
to.title = from.title;
|
|
32
|
+
to.prompt = from.prompt;
|
|
33
|
+
to.largeTitleDisplayMode = from.largeTitleDisplayMode;
|
|
34
|
+
to.standardAppearance = from.standardAppearance;
|
|
35
|
+
to.scrollEdgeAppearance = from.scrollEdgeAppearance;
|
|
36
|
+
to.compactAppearance = from.compactAppearance;
|
|
37
|
+
to.compactScrollEdgeAppearance = from.compactScrollEdgeAppearance;
|
|
38
|
+
to.hidesSearchBarWhenScrolling = from.hidesSearchBarWhenScrolling;
|
|
39
|
+
to.searchController =
|
|
40
|
+
from.searchController ? [[UISearchController alloc] initWithSearchResultsController:nil] : nil;
|
|
41
|
+
if (@available(iOS 16.0, *)) {
|
|
42
|
+
to.style = from.style;
|
|
43
|
+
to.preferredSearchBarPlacement = from.preferredSearchBarPlacement;
|
|
44
|
+
}
|
|
45
|
+
if (@available(iOS 26.0, *)) {
|
|
46
|
+
to.attributedSubtitle = from.attributedSubtitle;
|
|
47
|
+
to.largeAttributedSubtitle = from.largeAttributedSubtitle;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Everything the measure reads: the same answer while none of it changed.
|
|
52
|
+
static NSString *ANNPlaceSignature(UINavigationItem *item, UINavigationController *stack, BOOL showsTopBar,
|
|
53
|
+
BOOL showsToolbar) {
|
|
54
|
+
UIView *stackView = stack.view;
|
|
55
|
+
NSMutableString *signature = [NSMutableString
|
|
56
|
+
stringWithFormat:@"%@|%@|%ld|%p|%p|%p|%p|%p|%d|%d|%d|%d|%@|%@", item.title, item.prompt,
|
|
57
|
+
(long)item.largeTitleDisplayMode, item.standardAppearance,
|
|
58
|
+
item.scrollEdgeAppearance, item.compactAppearance,
|
|
59
|
+
item.compactScrollEdgeAppearance, item.searchController,
|
|
60
|
+
item.hidesSearchBarWhenScrolling, stack.navigationBar.prefersLargeTitles,
|
|
61
|
+
showsTopBar, showsToolbar, NSStringFromCGSize(stackView.bounds.size),
|
|
62
|
+
NSStringFromUIEdgeInsets(stackView.safeAreaInsets)];
|
|
63
|
+
[signature appendFormat:@"|%p|%ld", stack,
|
|
64
|
+
(long)stack.topViewController.navigationItem.largeTitleDisplayMode];
|
|
65
|
+
if (@available(iOS 16.0, *)) {
|
|
66
|
+
[signature appendFormat:@"|%ld|%ld", (long)item.style, (long)item.preferredSearchBarPlacement];
|
|
67
|
+
}
|
|
68
|
+
if (@available(iOS 26.0, *)) {
|
|
69
|
+
[signature appendFormat:@"|%@|%@", item.attributedSubtitle, item.largeAttributedSubtitle];
|
|
70
|
+
}
|
|
71
|
+
return signature;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@implementation ANNScreenPlace {
|
|
75
|
+
NSString *_signature;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
- (instancetype)initWithSafeArea:(UIEdgeInsets)safeArea
|
|
79
|
+
showsTopBar:(BOOL)showsTopBar
|
|
80
|
+
showsTabBar:(BOOL)showsTabBar
|
|
81
|
+
signature:(NSString *)signature {
|
|
82
|
+
if (self = [super init]) {
|
|
83
|
+
_safeArea = safeArea;
|
|
84
|
+
_showsTopBar = showsTopBar;
|
|
85
|
+
_showsTabBar = showsTabBar;
|
|
86
|
+
_signature = [signature copy];
|
|
87
|
+
}
|
|
88
|
+
return self;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
+ (ANNScreenPlace *)placeOfEntry:(UIViewController *)entry
|
|
92
|
+
inStack:(UINavigationController *)stack
|
|
93
|
+
options:(ANNNavigationOptions *)options {
|
|
94
|
+
ANNAssertMainQueue();
|
|
95
|
+
UIView *stackView = stack.viewIfLoaded;
|
|
96
|
+
UIWindow *window = stackView.window;
|
|
97
|
+
if (!window) {
|
|
98
|
+
return nil;
|
|
99
|
+
}
|
|
100
|
+
BOOL showsTopBar = [options.topBar.visible withDefault:YES];
|
|
101
|
+
BOOL showsToolbar = options.toolbar.shouldShow;
|
|
102
|
+
BOOL showsTabBar = !entry.hidesBottomBarWhenPushed &&
|
|
103
|
+
[[ANNBottomTabsController tabsOf:entry] showsTabBarForScreen:entry];
|
|
104
|
+
NSString *signature =
|
|
105
|
+
ANNPlaceSignature(entry.navigationItem, stack, showsTopBar, showsToolbar);
|
|
106
|
+
ANNScreenPlace *kept = objc_getAssociatedObject(entry, ANNScreenPlaceKey);
|
|
107
|
+
if (kept && kept.showsTabBar == showsTabBar && [kept->_signature isEqualToString:signature]) {
|
|
108
|
+
return kept;
|
|
109
|
+
}
|
|
110
|
+
UIEdgeInsets safeArea = [self measureEntry:entry
|
|
111
|
+
inStack:stack
|
|
112
|
+
showsTopBar:showsTopBar
|
|
113
|
+
showsToolbar:showsToolbar];
|
|
114
|
+
// The measure stands in the stack's view, whose safe area has the tab bar shown now: an entry that hides it gets
|
|
115
|
+
// the window's bottom there.
|
|
116
|
+
if (!showsTabBar) {
|
|
117
|
+
CGFloat tabBar = stackView.safeAreaInsets.bottom - window.safeAreaInsets.bottom;
|
|
118
|
+
if (tabBar > 0) {
|
|
119
|
+
safeArea.bottom = MAX(safeArea.bottom - tabBar, 0);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
ANNScreenPlace *place = [[ANNScreenPlace alloc] initWithSafeArea:safeArea
|
|
123
|
+
showsTopBar:showsTopBar
|
|
124
|
+
showsTabBar:showsTabBar
|
|
125
|
+
signature:signature];
|
|
126
|
+
objc_setAssociatedObject(entry, ANNScreenPlaceKey, place, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
127
|
+
return place;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The probe lays out at the stack's size in the stack's view, unseen and for this moment only, and leaves no search
|
|
131
|
+
// field behind.
|
|
132
|
+
+ (UIEdgeInsets)measureEntry:(UIViewController *)entry
|
|
133
|
+
inStack:(UINavigationController *)stack
|
|
134
|
+
showsTopBar:(BOOL)showsTopBar
|
|
135
|
+
showsToolbar:(BOOL)showsToolbar {
|
|
136
|
+
UINavigationController *probe = ANNPlaceProbeOf(stack);
|
|
137
|
+
UIViewController *below = probe.viewControllers.firstObject;
|
|
138
|
+
UIViewController *top = probe.topViewController;
|
|
139
|
+
UINavigationBar *bar = probe.navigationBar;
|
|
140
|
+
UINavigationBar *stackBar = stack.navigationBar;
|
|
141
|
+
bar.prefersLargeTitles = stackBar.prefersLargeTitles;
|
|
142
|
+
bar.standardAppearance = stackBar.standardAppearance;
|
|
143
|
+
bar.scrollEdgeAppearance = stackBar.scrollEdgeAppearance;
|
|
144
|
+
bar.compactAppearance = stackBar.compactAppearance;
|
|
145
|
+
below.navigationItem.largeTitleDisplayMode =
|
|
146
|
+
stack.topViewController.navigationItem.largeTitleDisplayMode;
|
|
147
|
+
ANNCopyBarItem(entry.navigationItem, top.navigationItem);
|
|
148
|
+
top.toolbarItems =
|
|
149
|
+
showsToolbar ? @[ [[UIBarButtonItem alloc] initWithTitle:@" "
|
|
150
|
+
style:UIBarButtonItemStylePlain
|
|
151
|
+
target:nil
|
|
152
|
+
action:nil] ]
|
|
153
|
+
: nil;
|
|
154
|
+
__block UIEdgeInsets safeArea = UIEdgeInsetsZero;
|
|
155
|
+
UIView *stackView = stack.view;
|
|
156
|
+
UIView *probeView = probe.view;
|
|
157
|
+
[UIView performWithoutAnimation:^{
|
|
158
|
+
[probe setNavigationBarHidden:!showsTopBar animated:NO];
|
|
159
|
+
[probe setToolbarHidden:!showsToolbar animated:NO];
|
|
160
|
+
probeView.alpha = 0;
|
|
161
|
+
probeView.userInteractionEnabled = NO;
|
|
162
|
+
probeView.accessibilityElementsHidden = YES;
|
|
163
|
+
probeView.frame = stackView.bounds;
|
|
164
|
+
[stackView insertSubview:probeView atIndex:0];
|
|
165
|
+
[probeView layoutIfNeeded];
|
|
166
|
+
[top.view layoutIfNeeded];
|
|
167
|
+
safeArea = top.view.safeAreaInsets;
|
|
168
|
+
[probeView removeFromSuperview];
|
|
169
|
+
}];
|
|
170
|
+
top.navigationItem.searchController = nil;
|
|
171
|
+
top.toolbarItems = nil;
|
|
172
|
+
return safeArea;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@end
|
|
@@ -15,8 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
15
15
|
/// `showInColumn`: the push's checks of the layout (the options of the tree, what the core makes), then the screen is
|
|
16
16
|
/// made and waits by the push's rules. Once it is ready, by the state then: the source in a split view not collapsed
|
|
17
17
|
/// into its compact layout — the split view shows the screen in `column`, unless a later request of that column began
|
|
18
|
-
/// meanwhile (the last one wins
|
|
19
|
-
///
|
|
18
|
+
/// meanwhile (the last one wins; the superseded one answers when the last one does, with its own screen's id whatever
|
|
19
|
+
/// the last one's outcome, as on Android); else the source in a stack — a push over it; else a rejection. The answer
|
|
20
|
+
/// is the shown screen's id; every rejection destroys the screen.
|
|
20
21
|
+ (void)showInColumn:(NSString *)column
|
|
21
22
|
layout:(ANNLayoutNode *)layout
|
|
22
23
|
componentId:(NSString *)componentId
|