@apexnative/navigation 1.0.0-alpha.5 → 1.0.0-alpha.6
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 +25 -0
- package/README.md +1 -1
- package/android/CLAUDE.md +14 -12
- package/android/CORE.md +9 -6
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +36 -5
- package/android/src/main/java/dev/apexnative/navigation/options/BottomTabsOptions.kt +13 -8
- package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +0 -2
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenEnvironment.kt +177 -0
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +12 -49
- package/android/src/main/java/dev/apexnative/navigation/react/ScrollEvents.kt +49 -128
- package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +6 -6
- package/android/src/main/java/dev/apexnative/navigation/utils/ViewExtensions.kt +0 -13
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +9 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsCollapseBehavior.kt +5 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +13 -10
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowGeometry.kt +22 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabsRoom.kt +0 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +7 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenSafeArea.kt +29 -11
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/externalcomponent/ExternalComponentViewController.kt +2 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +20 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +119 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +7 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetBack.kt +42 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +4 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/PreparedFrame.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +4 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +3 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ScrollEventListener.kt +6 -9
- package/android/src/main/java/dev/apexnative/navigation/views/animations/AnimationLayer.kt +73 -0
- package/android/src/main/java/dev/apexnative/navigation/views/animations/ValueInterpolation.kt +14 -0
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabsLayout.kt +61 -2
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomTabRow.kt +72 -10
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +9 -2
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +11 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +318 -25
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +18 -20
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +17 -25
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +15 -22
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +12 -9
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +84 -35
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +49 -40
- package/docs-md/architecture/transitions.md +4 -2
- package/docs-md/containers/modals.md +2 -0
- package/docs-md/containers/overlays.md +1 -1
- package/docs-md/containers/side-menu.md +1 -1
- package/docs-md/containers/tabs.md +2 -0
- package/docs-md/flows/bottom-sheets.md +1 -1
- package/docs-md/fundamentals/starting-the-app.md +1 -1
- package/docs-md/help/glossary.md +2 -2
- package/docs-md/help/troubleshooting.md +21 -0
- package/docs-md/index.json +12 -0
- package/docs-md/migration/from-expo-router.md +1 -1
- package/docs-md/migration/from-react-native-navigation.md +1 -1
- package/docs-md/migration/from-react-navigation.md +2 -2
- package/docs-md/screen-ui/back-and-gestures.md +14 -0
- package/docs-md/screen-ui/bar-buttons.md +4 -4
- package/docs-md/screen-ui/search.md +1 -1
- package/docs-md/screen-ui/system-ui.md +16 -11
- package/docs-md/screen-ui/toolbar-and-fab.md +2 -2
- package/docs-md/screen-ui/top-bar.md +8 -4
- package/ios/Animations/ANNFrameDriver.mm +2 -0
- package/ios/Animations/Elements/ANNSharedElementFlight.mm +70 -4
- package/ios/Animations/Interpolators/ANNInterpolate.h +2 -30
- package/ios/Animations/Interpolators/ANNTransformParts.h +48 -0
- package/ios/Animations/Interpolators/ANNTransformParts.mm +507 -0
- package/ios/Animations/Interpolators/ANNViewTracks.h +6 -4
- package/ios/Animations/Interpolators/ANNViewTracks.mm +49 -9
- package/ios/CLAUDE.md +17 -15
- package/ios/CORE.md +24 -20
- package/ios/Contract/ANNCommandRunner.h +8 -7
- package/ios/Contract/ANNCommandRunner.mm +21 -27
- package/ios/Contract/ANNLayoutNode.h +3 -3
- package/ios/Contract/ANNLayoutReader.h +3 -3
- package/ios/Core/ANNJavaScriptCalls.h +11 -12
- package/ios/Core/ANNJavaScriptCalls.mm +7 -16
- package/ios/Core/ANNReloadCoordinator.mm +4 -8
- package/ios/Core/ApexNavigation+Internal.h +1 -1
- package/ios/Layouts/Arrangement/ANNArrangementState.h +2 -17
- package/ios/Layouts/Arrangement/ANNArrangementState.mm +0 -38
- package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +4 -4
- package/ios/Layouts/Screen/ANNComponentViewController.h +2 -2
- package/ios/Layouts/Screen/ANNComponentViewController.mm +9 -22
- package/ios/Layouts/Screen/ANNPreviewInteraction.h +7 -4
- package/ios/Layouts/Screen/ANNPreviewInteraction.mm +72 -10
- package/ios/Layouts/Screen/ANNScreenEnvironment.h +63 -0
- package/ios/Layouts/Screen/ANNScreenEnvironment.mm +181 -0
- package/ios/Layouts/Screen/ANNScreenRegions.h +3 -18
- package/ios/Layouts/Screen/ANNScreenRegions.mm +0 -38
- package/ios/Layouts/Screen/ANNScreenSafeArea.h +4 -9
- package/ios/Layouts/Screen/ANNScreenSafeArea.mm +14 -16
- package/ios/Layouts/Stack/ANNPreviewPush.h +7 -6
- package/ios/Layouts/Stack/ANNPreviewPush.mm +57 -5
- package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.h +12 -1
- package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.mm +23 -0
- package/ios/Layouts/Tabs/ANNBottomTabsController.mm +2 -1
- package/ios/Layouts/Tabs/ANNBottomTabsCustomRow.mm +8 -3
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +4 -0
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +123 -36
- package/ios/Layouts/Tabs/ANNTabsDesign.h +6 -5
- package/ios/Layouts/Tabs/ANNTabsDesign.mm +9 -1
- package/ios/Layouts/Tabs/UITabBar+utils.h +5 -5
- package/ios/Layouts/Tabs/UITabBar+utils.mm +32 -13
- package/ios/Options/ANNBottomTabsOptions.h +2 -0
- package/ios/Options/ANNBottomTabsOptions.mm +1 -0
- package/ios/Options/ANNOptions.h +18 -0
- package/ios/Options/ANNOptions.mm +112 -1
- package/ios/Options/ANNOptionsReader.mm +110 -0
- package/ios/Options/Params/ANNParam.h +15 -0
- package/ios/Options/Params/ANNParam.mm +61 -0
- package/ios/Options/Params/Color.h +3 -0
- package/ios/Options/Params/Color.mm +21 -0
- package/ios/Options/Params/Image.h +4 -0
- package/ios/Options/Params/Image.mm +20 -0
- package/ios/React/ANNReactView.mm +6 -25
- package/ios/React/ANNSafeAreaViewComponentView.mm +26 -15
- package/ios/React/ANNSurfaceHostingView.h +6 -0
- package/ios/React/ANNSurfaceHostingView.mm +116 -0
- package/ios/TopBar/ANNBackButtonPresenter.mm +6 -5
- package/ios/TopBar/ANNBarAppearance.h +2 -1
- package/ios/TopBar/ANNBarAppearance.mm +111 -30
- package/ios/TopBar/ANNBarBackgroundComponent.mm +16 -0
- package/ios/TopBar/ANNBarButtonComponentView.h +4 -4
- package/ios/TopBar/ANNBarButtonComponentView.mm +1 -1
- package/ios/TopBar/ANNBarButtons.h +5 -3
- package/ios/TopBar/ANNBarButtons.mm +186 -17
- package/ios/TopBar/ANNLargeTitlePresenter.mm +24 -4
- package/ios/TopBar/ANNMainList.h +3 -0
- package/ios/TopBar/ANNMainList.mm +4 -0
- package/ios/TopBar/ANNScrollEdgeEffects.h +22 -6
- package/ios/TopBar/ANNScrollEdgeEffects.mm +123 -16
- package/ios/TopBar/ANNSearchPresenter.mm +2 -2
- package/ios/TopBar/ANNSearchResultsViewController.mm +5 -5
- package/ios/TopBar/ANNTopBarPresenter.h +3 -2
- package/ios/TopBar/ANNTopBarPresenter.mm +95 -22
- package/ios/TurboModules/ANNTurboModule.mm +20 -16
- package/lib/module/ARCHITECTURE.md +8 -4
- package/lib/module/api/hooks/useSafeAreaInsets.js +4 -3
- package/lib/module/api/hooks/useSafeAreaInsets.js.map +1 -1
- package/lib/module/api/options/OptionsChain.js +2 -2
- package/lib/module/engine/commands.js +21 -1
- package/lib/module/engine/commands.js.map +1 -1
- package/lib/module/engine/screens.js +29 -4
- package/lib/module/engine/screens.js.map +1 -1
- package/lib/typescript/api/app/createNavigation.d.ts +3 -3
- package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts +4 -3
- package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts.map +1 -1
- package/lib/typescript/api/options/OptionsChain.d.ts +2 -2
- package/lib/typescript/engine/commands.d.ts.map +1 -1
- package/lib/typescript/engine/screens.d.ts +20 -6
- package/lib/typescript/engine/screens.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +12 -7
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/bottomTabs.d.ts +18 -5
- package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/buttons.d.ts +8 -8
- package/lib/typescript/interfaces/options/system.d.ts +8 -5
- package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/toolbar.d.ts +2 -2
- package/lib/typescript/interfaces/options/topBar.d.ts +32 -29
- package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +8 -4
- package/src/api/app/createNavigation.ts +3 -3
- package/src/api/hooks/useSafeAreaInsets.ts +4 -3
- package/src/api/options/OptionsChain.ts +2 -2
- package/src/engine/commands.ts +30 -6
- package/src/engine/screens.tsx +29 -8
- package/src/interfaces/options/animations.ts +12 -7
- package/src/interfaces/options/bottomTabs.ts +18 -5
- package/src/interfaces/options/buttons.ts +8 -8
- package/src/interfaces/options/system.ts +8 -5
- package/src/interfaces/options/toolbar.ts +2 -2
- package/src/interfaces/options/topBar.ts +32 -29
- package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +0 -42
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +0 -70
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowPlacer.kt +0 -223
- package/ios/Layouts/Screen/ANNScreenInsets.h +0 -30
- package/ios/Layouts/Screen/ANNScreenInsets.mm +0 -70
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
#import "ANNScreenSafeArea.h"
|
|
2
2
|
#import "ANNScreenBars.h"
|
|
3
3
|
|
|
4
|
-
// A bar at the bottom covers more than the home indicator when the bottom inset passes it by more than this.
|
|
5
|
-
static const CGFloat ANNBottomBarThreshold = 0.5;
|
|
6
|
-
|
|
7
4
|
CGRect ANNScreenRootFrame(CGRect bounds, UIEdgeInsets safeArea, BOOL fromTop, BOOL toBottom) {
|
|
8
5
|
CGFloat top = fromTop ? 0 : safeArea.top;
|
|
9
6
|
CGFloat bottom = toBottom ? CGRectGetHeight(bounds) : CGRectGetHeight(bounds) - safeArea.bottom;
|
|
@@ -20,31 +17,32 @@ UIEdgeInsets ANNScreenInsetsOverRoot(CGRect rootFrame, CGSize viewSize, UIEdgeIn
|
|
|
20
17
|
|
|
21
18
|
UIEdgeInsets ANNScreenPadding(UIEdgeInsets insets, ANNNavigationOptions *options,
|
|
22
19
|
ANNScreenPaddingContext context) {
|
|
20
|
+
ANNTopBarOptions *topBar = options.topBar;
|
|
21
|
+
BOOL topBarShown = [topBar.visible withDefault:YES];
|
|
22
|
+
// Under a large title the list keeps its first rows clear of the bar itself. A padded top would follow the bar as
|
|
23
|
+
// it collapses and move the list with it, and UIKit, which collapses the title by the list's offset, would keep
|
|
24
|
+
// the small title hidden.
|
|
25
|
+
BOOL largeTitleShown = topBarShown && context.inStack && ANNLargeTitleCanShow(options);
|
|
23
26
|
NSArray<NSString *> *edges = options.layout.safeAreaEdges;
|
|
24
27
|
if (edges) {
|
|
25
|
-
// Exactly the edges given; unknown names count for nothing
|
|
26
|
-
|
|
28
|
+
// Exactly the edges given, but the top under a large title; unknown names count for nothing
|
|
29
|
+
// (`layout.safeAreaEdges`).
|
|
30
|
+
BOOL top = [edges containsObject:@"top"] && !largeTitleShown;
|
|
31
|
+
return UIEdgeInsetsMake(top ? insets.top : 0,
|
|
27
32
|
[edges containsObject:@"left"] ? insets.left : 0,
|
|
28
33
|
[edges containsObject:@"bottom"] ? insets.bottom : 0,
|
|
29
34
|
[edges containsObject:@"right"] ? insets.right : 0);
|
|
30
35
|
}
|
|
31
36
|
UIEdgeInsets padding = insets;
|
|
32
|
-
ANNTopBarOptions *topBar = options.topBar;
|
|
33
|
-
BOOL topBarShown = [topBar.visible withDefault:YES];
|
|
34
37
|
// A bar hidden for a while (`hideOnScroll`, `hideOnTap`, `hideOnKeyboard`) still counts as shown: only
|
|
35
38
|
// `topBar.visible: false` gives the content its top back. A large title that can show takes the top too: the list
|
|
36
39
|
// goes under the bar that collapses it.
|
|
37
|
-
if (topBarShown && context.inStack &&
|
|
38
|
-
(ANNScreenIsUnderTopBar(options) || ANNLargeTitleCanShow(options))) {
|
|
40
|
+
if (largeTitleShown || (topBarShown && context.inStack && ANNScreenIsUnderTopBar(options))) {
|
|
39
41
|
padding.top = 0;
|
|
40
42
|
}
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
if (insets.bottom > context.windowBottomSafeArea + ANNBottomBarThreshold ||
|
|
45
|
-
(context.inStack && context.stackShowsToolbar && [topBar.hideOnScroll withDefault:NO])) {
|
|
46
|
-
padding.bottom = 0;
|
|
47
|
-
}
|
|
43
|
+
// The bottom is free: the content goes down to the bottom of the screen, under the home indicator and under a bar
|
|
44
|
+
// at the bottom, and a list keeps its last rows clear with `useSafeAreaInsets()`.
|
|
45
|
+
padding.bottom = 0;
|
|
48
46
|
BOOL behindVerticalBar = [options.layout.drawBehindVerticalBar withDefault:YES] &&
|
|
49
47
|
[options.layout.verticalBars withDefault:YES];
|
|
50
48
|
if (behindVerticalBar) {
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
NS_ASSUME_NONNULL_BEGIN
|
|
7
7
|
|
|
8
|
-
/// The preview of a push (`usePreview`): a push that waits for a tap on its preview. Its screen is made
|
|
9
|
-
///
|
|
10
|
-
///
|
|
11
|
-
///
|
|
12
|
-
///
|
|
13
|
-
///
|
|
8
|
+
/// The preview of a push (`usePreview`): a push that waits for a tap on its preview. Its screen is made at once, out of
|
|
9
|
+
/// the stack, its key kept, and renders unseen: a React screen with a root of its own as a press on the view starts, a
|
|
10
|
+
/// native screen or one of the app's one tree at once. The view with `preview.nativeID` is looked for on the source
|
|
11
|
+
/// screen at once, then at each mount of the source's content (on each frame for a slot screen), for at most 0.5 s;
|
|
12
|
+
/// found, the preview module (`ANNPreviewInteraction`) shows the screen from it on a long press. A tap that commits
|
|
13
|
+
/// pushes the screen without an animation (and without a wait once its content is there), or goes back to the entry
|
|
14
|
+
/// with its key; a preview that goes without being opened destroys its screen and rejects the push. Main queue.
|
|
14
15
|
@interface ANNPreviewPush : NSObject
|
|
15
16
|
|
|
16
17
|
- (instancetype)init NS_UNAVAILABLE;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#import "ANNPreviewPush.h"
|
|
2
2
|
#import "ANNAssert.h"
|
|
3
|
+
#import "ANNCommandRunner.h"
|
|
4
|
+
#import "ANNComponentViewController.h"
|
|
3
5
|
#import "ANNErrorTexts.h"
|
|
6
|
+
#import "ANNExternalViewController.h"
|
|
4
7
|
#import "ANNLayoutFactory.h"
|
|
5
8
|
#import "ANNParking.h"
|
|
6
9
|
#import "ANNPopoverPresenter.h"
|
|
7
10
|
#import "ANNPreviewInteraction.h"
|
|
8
11
|
#import "ANNReloadCoordinator.h"
|
|
12
|
+
#import "ANNSlotContainers.h"
|
|
9
13
|
#import "ANNSlotScreenView.h"
|
|
10
14
|
#import "ANNStackCommands.h"
|
|
11
15
|
#import "ANNStackController.h"
|
|
@@ -34,6 +38,10 @@ static const NSTimeInterval ANNPreviewSearchInterval = 1.0 / 60;
|
|
|
34
38
|
@property(nonatomic, weak, nullable) UIView<ANNComponentProtocol> *watched;
|
|
35
39
|
@property(nonatomic) BOOL attached;
|
|
36
40
|
@property(nonatomic) BOOL done;
|
|
41
|
+
// The size of the preview, taken at the push.
|
|
42
|
+
@property(nonatomic) CGSize size;
|
|
43
|
+
// The screen rendered: at the push, or at the press for a screen of its own React root.
|
|
44
|
+
@property(nonatomic) BOOL rendered;
|
|
37
45
|
// The screen's unseen place in the main window, at its preview size, until UIKit takes its view for the menu.
|
|
38
46
|
@property(nonatomic, strong, nullable) UIView *host;
|
|
39
47
|
@property(nonatomic, strong, nullable) ANNParking *parking;
|
|
@@ -51,6 +59,15 @@ static NSMutableDictionary<NSString *, ANNPreviewRecord *> *ANNPreviewRecords(vo
|
|
|
51
59
|
return records;
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
// A React screen with a root of its own renders at the press: a view with a preview costs no React root, no render and
|
|
63
|
+
// no effects of the screen until the user presses it. A native screen (a kind of component screen) renders as it is
|
|
64
|
+
// made, and a screen of the app's one tree renders in JavaScript as its slot is added: both render at the push.
|
|
65
|
+
static BOOL ANNPreviewRendersAtPress(UIViewController *screen) {
|
|
66
|
+
return [screen isKindOfClass:ANNComponentViewController.class] &&
|
|
67
|
+
![screen isKindOfClass:ANNExternalViewController.class] &&
|
|
68
|
+
![screen.layoutInfo.name hasPrefix:ANNSlotPrefix];
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
@implementation ANNPreviewPush
|
|
55
72
|
|
|
56
73
|
+ (void)push:(ANNLayoutNode *)layout
|
|
@@ -71,6 +88,7 @@ static NSMutableDictionary<NSString *, ANNPreviewRecord *> *ANNPreviewRecords(vo
|
|
|
71
88
|
ANNPreviewRecord *record = [ANNPreviewRecord new];
|
|
72
89
|
record.previewId = layout.nodeId;
|
|
73
90
|
record.screen = screen;
|
|
91
|
+
record.size = size;
|
|
74
92
|
record.source = source;
|
|
75
93
|
record.answer = answer;
|
|
76
94
|
// The key applies only once the tap made the preview a screen of the stack.
|
|
@@ -81,7 +99,9 @@ static NSMutableDictionary<NSString *, ANNPreviewRecord *> *ANNPreviewRecords(vo
|
|
|
81
99
|
record.generation = answer.generation;
|
|
82
100
|
ANNPreviewRecords()[record.previewId] = record;
|
|
83
101
|
@try {
|
|
84
|
-
|
|
102
|
+
if (!ANNPreviewRendersAtPress(screen)) {
|
|
103
|
+
[self parkAndRender:record];
|
|
104
|
+
}
|
|
85
105
|
} @catch (NSException *exception) {
|
|
86
106
|
[self finish:record];
|
|
87
107
|
[screen destroy];
|
|
@@ -90,9 +110,11 @@ static NSMutableDictionary<NSString *, ANNPreviewRecord *> *ANNPreviewRecords(vo
|
|
|
90
110
|
[self startSearch:record];
|
|
91
111
|
}
|
|
92
112
|
|
|
93
|
-
// The screen renders
|
|
113
|
+
// The screen renders, once: its view waits unseen in the main window, at the size of the preview, so that its content
|
|
94
114
|
// lays out; nothing waits for it.
|
|
95
|
-
+ (void)parkAndRender:(ANNPreviewRecord *)record
|
|
115
|
+
+ (void)parkAndRender:(ANNPreviewRecord *)record {
|
|
116
|
+
record.rendered = YES;
|
|
117
|
+
CGSize size = record.size;
|
|
96
118
|
UIWindow *window = ApexNavigation.mainWindow;
|
|
97
119
|
if (window) {
|
|
98
120
|
UIView *host = [[UIView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)];
|
|
@@ -207,6 +229,9 @@ static NSMutableDictionary<NSString *, ANNPreviewRecord *> *ANNPreviewRecords(vo
|
|
|
207
229
|
record.attached = YES;
|
|
208
230
|
[self stopWatching:record];
|
|
209
231
|
__weak ANNPreviewRecord *weakRecord = record;
|
|
232
|
+
BOOL (^prepare)(void) = ^BOOL {
|
|
233
|
+
return [ANNPreviewPush renderAtPress:weakRecord];
|
|
234
|
+
};
|
|
210
235
|
BOOL (^commit)(void) = ^BOOL {
|
|
211
236
|
return [ANNPreviewPush commit:weakRecord];
|
|
212
237
|
};
|
|
@@ -217,10 +242,35 @@ static NSMutableDictionary<NSString *, ANNPreviewRecord *> *ANNPreviewRecords(vo
|
|
|
217
242
|
toView:view
|
|
218
243
|
reactTag:view.tag
|
|
219
244
|
source:source
|
|
245
|
+
prepare:prepare
|
|
220
246
|
commit:commit
|
|
221
247
|
dismissed:dismissed];
|
|
222
248
|
}
|
|
223
249
|
|
|
250
|
+
#pragma mark - The press
|
|
251
|
+
|
|
252
|
+
// The press starts (UIKit asks for the menu's configuration, about half a second before it shows the preview): the
|
|
253
|
+
// screen renders now if it hasn't yet, so that its content is there when the preview shows. Out of the command's
|
|
254
|
+
// runner, a failure rejects the push with its text and the preview goes. YES when the screen rendered. A preview of the
|
|
255
|
+
// JavaScript before a reload renders nothing: its screen would start in the new one.
|
|
256
|
+
+ (BOOL)renderAtPress:(ANNPreviewRecord *)record {
|
|
257
|
+
if (!record || record.done || !record.answer.isCurrent) {
|
|
258
|
+
return NO;
|
|
259
|
+
}
|
|
260
|
+
if (record.rendered) {
|
|
261
|
+
return YES;
|
|
262
|
+
}
|
|
263
|
+
@try {
|
|
264
|
+
[self parkAndRender:record];
|
|
265
|
+
} @catch (NSException *exception) {
|
|
266
|
+
[self finish:record];
|
|
267
|
+
[record.screen destroy];
|
|
268
|
+
[record.answer reject:[ANNCommandRunner rejectionOf:exception what:record.what]];
|
|
269
|
+
return NO;
|
|
270
|
+
}
|
|
271
|
+
return YES;
|
|
272
|
+
}
|
|
273
|
+
|
|
224
274
|
#pragma mark - The tap
|
|
225
275
|
|
|
226
276
|
// YES once the core took the preview: a key that leads back to an entry, or the push the queue accepted. From then on
|
|
@@ -267,7 +317,9 @@ static NSMutableDictionary<NSString *, ANNPreviewRecord *> *ANNPreviewRecords(vo
|
|
|
267
317
|
return YES;
|
|
268
318
|
}
|
|
269
319
|
// The preview grows into the screen: a push without an animation, and without a wait, as the screen rendered
|
|
270
|
-
// already; its answer at the end of the transaction.
|
|
320
|
+
// already; its answer at the end of the transaction. A screen that rendered only as the menu opened (a pointer's
|
|
321
|
+
// right click, then a quick tap) may have no content yet: its push waits for it as a push does.
|
|
322
|
+
BOOL waits = ANNPreviewRendersAtPress(screen) && !screen.reactView.isReady;
|
|
271
323
|
[stack setKey:key ofEntry:screen];
|
|
272
324
|
[stack.queue push:screen
|
|
273
325
|
answer:record.answer
|
|
@@ -275,7 +327,7 @@ static NSMutableDictionary<NSString *, ANNPreviewRecord *> *ANNPreviewRecords(vo
|
|
|
275
327
|
componentId:record.componentId
|
|
276
328
|
source:entry
|
|
277
329
|
animated:NO
|
|
278
|
-
waits:
|
|
330
|
+
waits:waits];
|
|
279
331
|
return YES;
|
|
280
332
|
}
|
|
281
333
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
#import "ANNReactView.h"
|
|
3
3
|
#import <UIKit/UIKit.h>
|
|
4
4
|
|
|
5
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* The content of the tab bar's bottom accessory (iOS 26): the React component fills it, and the
|
|
7
9
|
* component gets `bottomAccessoryPlacementChanged` when UIKit moves the accessory between the
|
|
@@ -10,8 +12,17 @@
|
|
|
10
12
|
@interface ANNBottomTabsAccessoryView : UIView
|
|
11
13
|
|
|
12
14
|
- (instancetype)initWithReactView:(ANNReactView *)reactView
|
|
13
|
-
eventEmitter:(ANNEventEmitter *)eventEmitter;
|
|
15
|
+
eventEmitter:(nullable ANNEventEmitter *)eventEmitter;
|
|
14
16
|
|
|
15
17
|
@property(nonatomic, strong, readonly) ANNReactView *reactView;
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* `handler` runs once when the view is out of a window: at once when it is already, else when it
|
|
21
|
+
* leaves it, so a replaced accessory shows to the end of whatever UIKit draws of it. nil cancels a
|
|
22
|
+
* handler not run yet.
|
|
23
|
+
*/
|
|
24
|
+
- (void)whenOutOfWindow:(nullable dispatch_block_t)handler;
|
|
25
|
+
|
|
17
26
|
@end
|
|
27
|
+
|
|
28
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
@implementation ANNBottomTabsAccessoryView {
|
|
4
4
|
ANNEventEmitter *_eventEmitter;
|
|
5
5
|
NSString *_placement;
|
|
6
|
+
dispatch_block_t _outOfWindowHandler;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
- (instancetype)initWithReactView:(ANNReactView *)reactView
|
|
@@ -32,6 +33,28 @@
|
|
|
32
33
|
}];
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
- (void)whenOutOfWindow:(dispatch_block_t)handler {
|
|
37
|
+
_outOfWindowHandler = nil;
|
|
38
|
+
if (!handler) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!self.window) {
|
|
42
|
+
handler();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
_outOfWindowHandler = [handler copy];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (void)didMoveToWindow {
|
|
49
|
+
[super didMoveToWindow];
|
|
50
|
+
dispatch_block_t handler = _outOfWindowHandler;
|
|
51
|
+
if (self.window || !handler) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
_outOfWindowHandler = nil;
|
|
55
|
+
handler();
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
// UIKit sets the environment when the accessory is shown and changes it as the tab bar minimizes
|
|
36
59
|
// and expands.
|
|
37
60
|
- (void)tabAccessoryEnvironmentDidChange API_AVAILABLE(ios(26.0)) {
|
|
@@ -598,10 +598,11 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
|
|
|
598
598
|
[[self tabsPresenter] tabsDidLayOut];
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
// `afterInitialTab` goes on at the tabs' first appearance at the latest.
|
|
601
|
+
// `afterInitialTab` goes on at the tabs' first appearance at the latest; a dot that waited for the window shows.
|
|
602
602
|
- (void)viewDidAppear:(BOOL)animated {
|
|
603
603
|
[super viewDidAppear:animated];
|
|
604
604
|
[self scheduleChain];
|
|
605
|
+
[[self tabsPresenter] tabsDidAppear];
|
|
605
606
|
}
|
|
606
607
|
|
|
607
608
|
#pragma mark - Rendering and the preload window
|
|
@@ -126,8 +126,13 @@ static const CGFloat ANNWideRowMargin = 16;
|
|
|
126
126
|
if (self.superview != host) {
|
|
127
127
|
[host addSubview:self];
|
|
128
128
|
}
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
if (host.subviews.lastObject != self) {
|
|
130
|
+
[host bringSubviewToFront:self];
|
|
131
|
+
}
|
|
132
|
+
// The system bar keeps its place and shows nothing; UIKit makes its subviews anew on a size change. Not the bar's
|
|
133
|
+
// `alpha`: UIKit puts it back to 1 when the bar shows, on a pop with `hidesBottomBarWhenPushed`, and on iOS 27 on
|
|
134
|
+
// a rotation and a theme change, after the tabs' layout (a check here misses it); alpha 0 also drops the system's
|
|
135
|
+
// soft fade at the bottom.
|
|
131
136
|
for (UIView *subview in bar.subviews) {
|
|
132
137
|
if (!subview.hidden) {
|
|
133
138
|
subview.hidden = YES;
|
|
@@ -193,6 +198,7 @@ static const CGFloat ANNWideRowMargin = 16;
|
|
|
193
198
|
extra = MAX(0, CGRectGetMinY(barFrame) - CGRectGetMinY(frame));
|
|
194
199
|
}
|
|
195
200
|
BOOL hidden = wide ? _hiddenByOptions : (tabs.barHidden || bar.hidden);
|
|
201
|
+
// A new size lays the cells out again by itself; the same frame changes nothing.
|
|
196
202
|
if (!CGRectEqualToRect(self.frame, frame)) {
|
|
197
203
|
self.frame = frame;
|
|
198
204
|
}
|
|
@@ -203,7 +209,6 @@ static const CGFloat ANNWideRowMargin = 16;
|
|
|
203
209
|
_background.layer.cornerRadius = radius;
|
|
204
210
|
}
|
|
205
211
|
[self setExtraSafeArea:hidden ? 0 : extra];
|
|
206
|
-
[self setNeedsLayout];
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
- (void)setHiddenByOptions:(BOOL)hiddenByOptions {
|
|
@@ -55,6 +55,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
55
55
|
/// The tabs laid out (and before they appear): the dots, the buttons' `testID`s, the custom row.
|
|
56
56
|
- (void)tabsDidLayOut;
|
|
57
57
|
|
|
58
|
+
/// The tabs appeared, in a window: a dot set while they were out of it (under a modal over the whole screen) is
|
|
59
|
+
/// placed now, as no layout may follow.
|
|
60
|
+
- (void)tabsDidAppear;
|
|
61
|
+
|
|
58
62
|
/// The bar laid out in a window for the first time: the titles are set again, so that the bar measures them at their
|
|
59
63
|
/// real place.
|
|
60
64
|
- (void)barDidLayOutInWindow;
|
|
@@ -12,9 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
// The dot of a tab before iOS 26: its default size and color.
|
|
14
14
|
static const CGFloat ANNDotDefaultSize = 6;
|
|
15
|
-
// A button not there yet gets its `testID` again this often, at most this many times (`bottomTab.testID`).
|
|
16
|
-
static const NSTimeInterval ANNTestIDRetryInterval = 0.15;
|
|
17
|
-
static const NSUInteger ANNTestIDRetries = 5;
|
|
18
15
|
|
|
19
16
|
// A picture of `color` (both of its styles when it is dynamic): the bar's 1×1 background and the border's line.
|
|
20
17
|
static UIImage *ANNImageOfColor(UIColor *color, CGSize size) {
|
|
@@ -63,13 +60,23 @@ static id ANNValue(NSArray *list, NSUInteger index) {
|
|
|
63
60
|
NSMutableArray<NSNumber *> *_roleItems;
|
|
64
61
|
// The identifiers the buttons had before the core gave them a `testID`, to give back.
|
|
65
62
|
NSMapTable<UIView *, NSString *> *_buttonIds;
|
|
63
|
+
// What the `testID`s and the dots were last applied to: the bar's items, the button of each tab and of More, the
|
|
64
|
+
// bar's size and its traits. UIKit makes the buttons anew (rotation, text size, items moving between the bar and
|
|
65
|
+
// More), and a layout that finds other buttons applies them again.
|
|
66
|
+
NSPointerArray *_appliedButtons;
|
|
67
|
+
CGSize _appliedBarSize;
|
|
68
|
+
UITraitCollection *_appliedBarTraits;
|
|
66
69
|
BOOL _tabsAreSet;
|
|
67
70
|
BOOL _titlesHidden;
|
|
71
|
+
// A dot that shows was placed while its button had no window (the tabs under a full-screen modal): it hides then,
|
|
72
|
+
// and the next layout in the window places it, whatever the check of the buttons says.
|
|
73
|
+
BOOL _dotsWaitForWindow;
|
|
68
74
|
// The bar's look: its options, and the background and border every item's look starts from.
|
|
69
75
|
ANNBottomTabsOptions *_barOptions;
|
|
70
76
|
UITabBarAppearance *_base;
|
|
71
77
|
ANNBottomTabsCustomRow *_row;
|
|
72
|
-
// The accessory shown (its id), and the content view of each accessory made
|
|
78
|
+
// The accessory shown (its id), and the content view of each accessory made: kept while hidden, until another
|
|
79
|
+
// accessory shows or the tabs go.
|
|
73
80
|
NSString *_accessoryId;
|
|
74
81
|
NSMutableDictionary<NSString *, ANNBottomTabsAccessoryView *> *_accessoryViews;
|
|
75
82
|
// The pictures of the bar's colors by color and size.
|
|
@@ -123,6 +130,7 @@ static id ANNValue(NSArray *list, NSUInteger index) {
|
|
|
123
130
|
_dotViews = [NSMutableArray new];
|
|
124
131
|
_roleItems = [NSMutableArray new];
|
|
125
132
|
_buttonIds = [NSMapTable weakToStrongObjectsMapTable];
|
|
133
|
+
_appliedButtons = [NSPointerArray weakObjectsPointerArray];
|
|
126
134
|
_accessoryViews = [NSMutableDictionary new];
|
|
127
135
|
_colorImages = [NSMutableDictionary new];
|
|
128
136
|
_iconLoads = [NSMutableDictionary new];
|
|
@@ -251,7 +259,7 @@ static id ANNValue(NSArray *list, NSUInteger index) {
|
|
|
251
259
|
[_row applyOptions:resolvedOptions.bottomTabs.customRow];
|
|
252
260
|
}
|
|
253
261
|
[self applyAccessoryOf:resolvedOptions animated:NO];
|
|
254
|
-
[self
|
|
262
|
+
[self applyButtonTestIDs];
|
|
255
263
|
}
|
|
256
264
|
|
|
257
265
|
#pragma mark - ANNNodePresenter
|
|
@@ -315,7 +323,7 @@ static id ANNValue(NSArray *list, NSUInteger index) {
|
|
|
315
323
|
}
|
|
316
324
|
[self buildItemAtIndex:index bottomTab:bottomTab updatesBadgeAndDot:badgeAndDot];
|
|
317
325
|
[self updateCells];
|
|
318
|
-
[self
|
|
326
|
+
[self applyButtonTestIDs];
|
|
319
327
|
}
|
|
320
328
|
|
|
321
329
|
// The whole item from `bottomTab`; the badge and the dot from it only with `updatesBadgeAndDot`.
|
|
@@ -589,6 +597,7 @@ static ANNBottomTabOptions *ANNWithIcons(ANNBottomTabOptions *bottomTab, NSArray
|
|
|
589
597
|
if (anyDot) {
|
|
590
598
|
[bar layoutIfNeeded];
|
|
591
599
|
}
|
|
600
|
+
BOOL waitsForWindow = NO;
|
|
592
601
|
for (NSUInteger index = 0; index < tabs.layoutTabs.count; index++) {
|
|
593
602
|
ANNTabDotView *view = ANNValue(_dotViews, index);
|
|
594
603
|
if (!view) {
|
|
@@ -602,49 +611,36 @@ static ANNBottomTabOptions *ANNWithIcons(ANNBottomTabOptions *bottomTab, NSArray
|
|
|
602
611
|
}
|
|
603
612
|
UITabBarItem *item = tabs.layoutTabs[index].tabBarItem;
|
|
604
613
|
BOOL inBar = [bar.items indexOfObjectIdenticalTo:item] != NSNotFound;
|
|
605
|
-
|
|
614
|
+
UIView *button = inBar ? [bar viewOfItem:item] : nil;
|
|
615
|
+
waitsForWindow = waitsForWindow || (inBar && !button.window);
|
|
616
|
+
[view placeOverButton:button inBar:bar];
|
|
606
617
|
}
|
|
618
|
+
_dotsWaitForWindow = waitsForWindow;
|
|
607
619
|
}
|
|
608
620
|
|
|
609
621
|
#pragma mark - testID (`bottomTab.testID`, `bottomTabs.moreTestID`)
|
|
610
622
|
|
|
611
|
-
// Each tab's button carries its `testID`; a
|
|
612
|
-
//
|
|
613
|
-
- (void)
|
|
623
|
+
// Each tab's button carries its `testID`; a removed `testID` gives the button its identifier back. A button not
|
|
624
|
+
// there yet gets it at the layout that finds it (`tabsDidLayOut`).
|
|
625
|
+
- (void)applyButtonTestIDs {
|
|
614
626
|
ANNBottomTabsController *tabs = self.tabs;
|
|
615
627
|
UITabBar *bar = tabs.tabBar;
|
|
616
628
|
if (!tabs || !bar) {
|
|
617
629
|
return;
|
|
618
630
|
}
|
|
619
|
-
BOOL missing = NO;
|
|
620
631
|
for (NSUInteger index = 0; index < tabs.layoutTabs.count; index++) {
|
|
621
632
|
UITabBarItem *item = tabs.layoutTabs[index].tabBarItem;
|
|
622
|
-
NSString *testID = item.accessibilityIdentifier;
|
|
623
633
|
UIView *button = [bar viewOfItem:item];
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
continue;
|
|
634
|
+
if (button) {
|
|
635
|
+
[self setTestID:item.accessibilityIdentifier ofButton:button];
|
|
627
636
|
}
|
|
628
|
-
[self setTestID:testID ofButton:button];
|
|
629
637
|
}
|
|
630
638
|
if (tabs.layoutTabs.count > 5) {
|
|
631
|
-
NSString *moreTestID = [_barOptions.moreTestID withDefault:nil];
|
|
632
639
|
UIView *button = [bar viewOfItem:tabs.moreNavigationController.tabBarItem];
|
|
633
640
|
if (button) {
|
|
634
|
-
[self setTestID:moreTestID ofButton:button];
|
|
635
|
-
} else {
|
|
636
|
-
missing = missing || moreTestID.length > 0;
|
|
641
|
+
[self setTestID:[_barOptions.moreTestID withDefault:nil] ofButton:button];
|
|
637
642
|
}
|
|
638
643
|
}
|
|
639
|
-
if (!missing || retries == 0) {
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
__weak ANNBottomTabsPresenter *weakSelf = self;
|
|
643
|
-
dispatch_after(
|
|
644
|
-
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ANNTestIDRetryInterval * NSEC_PER_SEC)),
|
|
645
|
-
dispatch_get_main_queue(), ^{
|
|
646
|
-
[weakSelf applyButtonTestIDsWithRetries:retries - 1];
|
|
647
|
-
});
|
|
648
644
|
}
|
|
649
645
|
|
|
650
646
|
- (void)setTestID:(NSString *)testID ofButton:(UIView *)button {
|
|
@@ -728,8 +724,9 @@ static ANNBottomTabOptions *ANNWithIcons(ANNBottomTabOptions *bottomTab, NSArray
|
|
|
728
724
|
warned = YES;
|
|
729
725
|
RCTLogWarn(
|
|
730
726
|
@"[ApexNavigation] bottomTabs.backgroundColor is ignored with Liquid "
|
|
731
|
-
@"Glass (iOS 26). It applies
|
|
732
|
-
@"earlier iOS
|
|
727
|
+
@"Glass (iOS 26). It applies before iOS 26 and where UIKit keeps the "
|
|
728
|
+
@"earlier design for UIDesignRequiresCompatibility (iOS 26; iOS 27 only "
|
|
729
|
+
@"for an app built with an SDK before iOS 27).");
|
|
733
730
|
}
|
|
734
731
|
}
|
|
735
732
|
}
|
|
@@ -969,14 +966,15 @@ static ANNBottomTabOptions *ANNWithIcons(ANNBottomTabOptions *bottomTab, NSArray
|
|
|
969
966
|
[tabs updateRootLayoutOfShownScreens];
|
|
970
967
|
}
|
|
971
968
|
if (mergeOptions.testID.hasValue || mergeOptions.moreTestID.hasValue) {
|
|
972
|
-
[self
|
|
969
|
+
[self applyButtonTestIDs];
|
|
973
970
|
}
|
|
974
971
|
}
|
|
975
972
|
|
|
976
973
|
#pragma mark - The accessory (iOS 26)
|
|
977
974
|
|
|
978
|
-
// No name or `visible: false`: no accessory. The same id shown: nothing; another: a new one. A
|
|
979
|
-
//
|
|
975
|
+
// No name or `visible: false`: no accessory. The same id shown: nothing; another: a new one. A hidden accessory keeps
|
|
976
|
+
// its root, so one hidden and shown again keeps its state; the roots of the other ids stop once another accessory
|
|
977
|
+
// shows and theirs is out of the window.
|
|
980
978
|
- (void)applyAccessoryOf:(ANNNavigationOptions *)options animated:(BOOL)animated {
|
|
981
979
|
if (@available(iOS 26.0, *)) {
|
|
982
980
|
ANNBottomTabsController *tabs = self.tabs;
|
|
@@ -1012,8 +1010,37 @@ static ANNBottomTabOptions *ANNWithIcons(ANNBottomTabOptions *bottomTab, NSArray
|
|
|
1012
1010
|
return;
|
|
1013
1011
|
}
|
|
1014
1012
|
_accessoryId = componentId;
|
|
1013
|
+
[view whenOutOfWindow:nil];
|
|
1015
1014
|
UITabAccessory *tabAccessory = [[UITabAccessory alloc] initWithContentView:view];
|
|
1016
1015
|
[tabs setBottomAccessory:tabAccessory animated:animated && inWindow];
|
|
1016
|
+
[self stopAccessoriesOtherThan:componentId];
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// The roots of the accessories not shown stop, each once its view is out of the window, unless it shows again first.
|
|
1021
|
+
- (void)stopAccessoriesOtherThan:(NSString *)shownId {
|
|
1022
|
+
ANNBottomTabsController *tabs = self.tabs;
|
|
1023
|
+
__weak ANNBottomTabsPresenter *weakSelf = self;
|
|
1024
|
+
__weak ANNReactComponentRegistry *weakComponents = tabs.components;
|
|
1025
|
+
for (NSString *componentId in _accessoryViews.allKeys) {
|
|
1026
|
+
if ([componentId isEqualToString:shownId]) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
ANNBottomTabsAccessoryView *view = _accessoryViews[componentId];
|
|
1030
|
+
__weak ANNBottomTabsAccessoryView *weakView = view;
|
|
1031
|
+
[view whenOutOfWindow:^{
|
|
1032
|
+
ANNBottomTabsPresenter *presenter = weakSelf;
|
|
1033
|
+
ANNBottomTabsAccessoryView *gone = weakView;
|
|
1034
|
+
if (!presenter || !gone || presenter->_accessoryViews[componentId] != gone ||
|
|
1035
|
+
[presenter->_accessoryId isEqualToString:componentId]) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
[presenter->_accessoryViews removeObjectForKey:componentId];
|
|
1039
|
+
ANNReactComponentRegistry *components = weakComponents;
|
|
1040
|
+
if ([components existingComponent:componentId] == gone.reactView) {
|
|
1041
|
+
[components removeChildComponent:componentId];
|
|
1042
|
+
}
|
|
1043
|
+
}];
|
|
1017
1044
|
}
|
|
1018
1045
|
}
|
|
1019
1046
|
|
|
@@ -1062,18 +1089,78 @@ static ANNBottomTabOptions *ANNWithIcons(ANNBottomTabOptions *bottomTab, NSArray
|
|
|
1062
1089
|
[self updateCells];
|
|
1063
1090
|
}
|
|
1064
1091
|
|
|
1092
|
+
// The `testID`s and the dots go on the buttons again only when the bar has other buttons than they were applied to,
|
|
1093
|
+
// another size or other traits; a merge and a rebuilt item apply them at once. A dot placed out of the window waits
|
|
1094
|
+
// for the next layout.
|
|
1065
1095
|
- (void)tabsDidLayOut {
|
|
1066
1096
|
if (!_tabsAreSet) {
|
|
1067
1097
|
return;
|
|
1068
1098
|
}
|
|
1069
1099
|
[self updateHiddenTitles];
|
|
1070
|
-
|
|
1100
|
+
BOOL changed = [self barChangedSinceApplied];
|
|
1101
|
+
if ((changed || _dotsWaitForWindow) && !ANNTabsDesign.isGlass) {
|
|
1071
1102
|
[self placeDots];
|
|
1072
1103
|
}
|
|
1073
|
-
|
|
1104
|
+
if (changed) {
|
|
1105
|
+
[self applyButtonTestIDs];
|
|
1106
|
+
}
|
|
1074
1107
|
[_row layOutInTabs];
|
|
1075
1108
|
}
|
|
1076
1109
|
|
|
1110
|
+
- (void)tabsDidAppear {
|
|
1111
|
+
if (_tabsAreSet && _dotsWaitForWindow && !ANNTabsDesign.isGlass) {
|
|
1112
|
+
[self placeDots];
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// The bar's items and the button of each tab and of More, by pointer, against those last seen; no walk through the
|
|
1117
|
+
// buttons. An item moving between the bar and More changes the items. The tabs lay out before the bar does: a tab in
|
|
1118
|
+
// the bar without its button yet makes the bar lay out first.
|
|
1119
|
+
- (BOOL)barChangedSinceApplied {
|
|
1120
|
+
ANNBottomTabsController *tabs = self.tabs;
|
|
1121
|
+
UITabBar *bar = tabs.tabBar;
|
|
1122
|
+
if (!tabs || !bar) {
|
|
1123
|
+
return NO;
|
|
1124
|
+
}
|
|
1125
|
+
NSArray<UIViewController *> *layoutTabs = tabs.layoutTabs;
|
|
1126
|
+
NSArray<UITabBarItem *> *barItems = bar.items;
|
|
1127
|
+
NSUInteger tabCount = layoutTabs.count + (layoutTabs.count > 5 ? 1 : 0);
|
|
1128
|
+
NSMutableArray *seen = [NSMutableArray arrayWithArray:barItems];
|
|
1129
|
+
BOOL laidOut = NO;
|
|
1130
|
+
for (NSUInteger index = 0; index < tabCount; index++) {
|
|
1131
|
+
UITabBarItem *item = index < layoutTabs.count ? layoutTabs[index].tabBarItem
|
|
1132
|
+
: tabs.moreNavigationController.tabBarItem;
|
|
1133
|
+
UIView *button = [bar viewOfItem:item];
|
|
1134
|
+
if (!button && !laidOut && bar.window &&
|
|
1135
|
+
[barItems indexOfObjectIdenticalTo:item] != NSNotFound) {
|
|
1136
|
+
laidOut = YES;
|
|
1137
|
+
[bar layoutIfNeeded];
|
|
1138
|
+
button = [bar viewOfItem:item];
|
|
1139
|
+
}
|
|
1140
|
+
[seen addObject:button ?: NSNull.null];
|
|
1141
|
+
}
|
|
1142
|
+
NSUInteger count = seen.count;
|
|
1143
|
+
BOOL changed = _appliedButtons.count != count;
|
|
1144
|
+
for (NSUInteger index = 0; index < count && !changed; index++) {
|
|
1145
|
+
id object = seen[index] == NSNull.null ? nil : seen[index];
|
|
1146
|
+
changed = (__bridge void *)object != [_appliedButtons pointerAtIndex:index];
|
|
1147
|
+
}
|
|
1148
|
+
CGSize size = bar.bounds.size;
|
|
1149
|
+
UITraitCollection *traits = bar.traitCollection;
|
|
1150
|
+
changed = changed || !CGSizeEqualToSize(size, _appliedBarSize) ||
|
|
1151
|
+
![traits isEqual:_appliedBarTraits];
|
|
1152
|
+
if (!changed) {
|
|
1153
|
+
return NO;
|
|
1154
|
+
}
|
|
1155
|
+
_appliedButtons.count = 0;
|
|
1156
|
+
for (id object in seen) {
|
|
1157
|
+
[_appliedButtons addPointer:object == NSNull.null ? NULL : (__bridge void *)object];
|
|
1158
|
+
}
|
|
1159
|
+
_appliedBarSize = size;
|
|
1160
|
+
_appliedBarTraits = traits;
|
|
1161
|
+
return YES;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1077
1164
|
// The glass bar measures a title when it is set and keeps that size: a title set before the bar was ever in a window
|
|
1078
1165
|
// was measured in a button of its first layout there, when the bar's floating platter had no size yet, and stays cut
|
|
1079
1166
|
// short ("Sec…") and lower than a title measured later. Neither a layout of the bar nor of its buttons measures it
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
NS_ASSUME_NONNULL_BEGIN
|
|
4
4
|
|
|
5
|
-
/// Which design UIKit draws the bars in: the Liquid Glass of iOS 26, or the design before it, which
|
|
6
|
-
/// the app's Info.plist has `UIDesignRequiresCompatibility = YES` (the compatibility mode)
|
|
7
|
-
///
|
|
8
|
-
///
|
|
5
|
+
/// Which design UIKit draws the bars in: the Liquid Glass of iOS 26, or the design before it, which UIKit keeps when
|
|
6
|
+
/// the app's Info.plist has `UIDesignRequiresCompatibility = YES` (the compatibility mode): on iOS 26, and on iOS 27
|
|
7
|
+
/// only for an app built with an SDK before iOS 27. The key is read once per process. Every place of the core that
|
|
8
|
+
/// depends on the design asks here.
|
|
9
9
|
@interface ANNTabsDesign : NSObject
|
|
10
10
|
|
|
11
11
|
/// iOS 26 or later, and not the compatibility mode.
|
|
12
12
|
@property(class, nonatomic, readonly) BOOL isGlass;
|
|
13
13
|
|
|
14
|
-
/// iOS 26 or later with `UIDesignRequiresCompatibility = YES
|
|
14
|
+
/// iOS 26 or later with `UIDesignRequiresCompatibility = YES`, where UIKit honors it (not on iOS 27 and later for an
|
|
15
|
+
/// app built with the iOS 27 SDK): the bars look as before iOS 26.
|
|
15
16
|
@property(class, nonatomic, readonly) BOOL isCompatibilityMode;
|
|
16
17
|
|
|
17
18
|
/// iOS 18 and later on an iPad or a Mac in a regular width: the system's tab bar is on top (or in the sidebar), and
|