@apexnative/navigation 1.0.0-alpha.1 → 1.0.0-alpha.2
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 +16 -0
- package/README.md +147 -51
- package/android/CLAUDE.md +10 -4
- package/android/CORE.md +4 -1
- package/android/RECIPES.md +4 -3
- package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/react/ErrorTexts.kt +75 -60
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +3 -2
- package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +7 -6
- package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +3 -35
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventContract.kt +226 -0
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +101 -87
- package/android/src/main/java/dev/apexnative/navigation/react/events/ScreenAppearance.kt +0 -6
- package/android/src/main/java/dev/apexnative/navigation/utils/UiUtils.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabItems.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +12 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +12 -11
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +25 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +4 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuPresenter.kt +16 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +8 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +2 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +2 -1
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +5 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStep.kt +31 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +85 -27
- package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionTargets.kt +9 -6
- package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionViews.kt +100 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +45 -1
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +2 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +4 -6
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +4 -6
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +4 -3
- package/cli/app.js +1 -1
- package/cli/project.js +6 -2
- package/cli/setup/index.js +13 -0
- package/cli/setup/steps/appDelegate.js +66 -11
- package/cli/setup/steps/sceneDelegate.js +108 -0
- package/docs-md/architecture/transitions.md +1 -1
- package/docs-md/guides/deep-links.md +15 -33
- package/docs-md/start/installation.md +1 -0
- package/ios/Animations/ANNTransitionAnimator.mm +13 -11
- package/ios/Animations/ANNTransitionWait.mm +6 -7
- package/ios/Animations/Elements/ANNElementFinder.h +57 -0
- package/ios/Animations/Elements/ANNElementFinder.mm +174 -2
- package/ios/Animations/Elements/ANNElementTransition.h +7 -5
- package/ios/Animations/Elements/ANNElementTransition.mm +17 -19
- package/ios/Animations/Elements/ANNSharedElementFlight.h +13 -8
- package/ios/Animations/Elements/ANNSharedElementFlight.mm +59 -24
- package/ios/CLAUDE.md +10 -2
- package/ios/CORE.md +3 -2
- package/ios/Contract/ANNCommandAnswer.h +4 -3
- package/ios/Contract/ANNCommandAnswer.mm +12 -2
- package/ios/Contract/ANNCommandRunner.h +2 -2
- package/ios/Contract/ANNCommandRunner.mm +1 -1
- package/ios/Contract/ANNErrorTexts.h +6 -2
- package/ios/Contract/ANNErrorTexts.mm +18 -10
- package/ios/Core/ANNDeepLinkQueue.h +20 -1
- package/ios/Core/ANNDeepLinkQueue.mm +107 -12
- package/ios/Core/ANNErrorHandler.h +8 -3
- package/ios/Core/ANNErrorHandler.mm +17 -1
- package/ios/Core/ANNEventContract.h +246 -0
- package/ios/Core/ANNEventContract.mm +225 -0
- package/ios/Core/ANNEventEmitter.h +5 -1
- package/ios/Core/ANNEventEmitter.mm +117 -80
- package/ios/Core/ANNHinge.mm +6 -4
- package/ios/Core/ANNSceneDelegate.mm +25 -15
- package/ios/Core/ANNSystemUI.h +3 -2
- package/ios/Core/ANNSystemUI.mm +7 -0
- package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +6 -4
- package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
- package/ios/Layouts/Modal/ANNModalPresentation.h +8 -5
- package/ios/Layouts/Modal/ANNModalPresentation.mm +12 -12
- package/ios/Layouts/Modal/ANNSheetPresenter.mm +83 -1
- package/ios/Layouts/Root/ANNRootCoordinator.mm +12 -3
- package/ios/Layouts/Screen/ANNComponentPresenter.h +11 -3
- package/ios/Layouts/Screen/ANNComponentPresenter.mm +34 -6
- package/ios/Layouts/Screen/ANNComponentViewController.mm +28 -7
- package/ios/Layouts/Screen/ANNNativeScreenContext.mm +7 -5
- package/ios/Layouts/Screen/ANNScreenInsets.mm +6 -4
- package/ios/Layouts/Screen/ANNScreenRegions.mm +9 -6
- package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -7
- package/ios/Options/ANNSheetOptions.h +4 -1
- package/ios/Public/ApexNavigation/ANNSceneDelegate.h +3 -0
- package/ios/RECIPES.md +2 -1
- package/ios/TurboModules/ANNTurboEventEmitter.mm +4 -35
- package/ios/TurboModules/ANNTurboModule.mm +5 -2
- package/lib/module/ARCHITECTURE.md +7 -4
- package/lib/module/api/app/createNavigation.js +2 -0
- package/lib/module/api/app/createNavigation.js.map +1 -1
- package/lib/module/api/linking/dispatchedLinks.js +23 -0
- package/lib/module/api/linking/dispatchedLinks.js.map +1 -0
- package/lib/module/interfaces/contract/errors.json +275 -0
- package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
- package/lib/typescript/api/linking/dispatchedLinks.d.ts +7 -0
- package/lib/typescript/api/linking/dispatchedLinks.d.ts.map +1 -0
- package/lib/typescript/interfaces/Events.d.ts +9 -0
- package/lib/typescript/interfaces/Events.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +17 -13
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/modals.d.ts +15 -11
- package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/sideMenu.d.ts +7 -6
- package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/system.d.ts +4 -3
- package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +7 -4
- package/src/api/app/createNavigation.ts +2 -0
- package/src/api/linking/dispatchedLinks.ts +18 -0
- package/src/interfaces/Events.ts +10 -0
- package/src/interfaces/contract/errors.json +275 -0
- package/src/interfaces/options/animations.ts +17 -13
- package/src/interfaces/options/modals.ts +15 -11
- package/src/interfaces/options/sideMenu.ts +7 -6
- package/src/interfaces/options/system.ts +4 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('./types').StepResult} StepResult */
|
|
5
|
+
|
|
6
|
+
const HOW_TO = `Let ANNSceneDelegate take the app's scene: it moves the navigation's window into the scene and passes the
|
|
7
|
+
scene's links to the app delegate. React Native starts in ANNAppDelegate, not here.
|
|
8
|
+
|
|
9
|
+
import ApexNavigation
|
|
10
|
+
import UIKit
|
|
11
|
+
|
|
12
|
+
class SceneDelegate: ANNSceneDelegate {}
|
|
13
|
+
|
|
14
|
+
A scene delegate of the app's own calls ApexNavigation.attach(to:) in scene(_:willConnectTo:options:) instead, as the
|
|
15
|
+
deep links guide shows.`;
|
|
16
|
+
|
|
17
|
+
const SET_UP = `import ApexNavigation
|
|
18
|
+
import UIKit
|
|
19
|
+
|
|
20
|
+
/// ANNSceneDelegate moves the navigation's window into the app's scene and passes the scene's links to the app delegate.
|
|
21
|
+
class SceneDelegate: ANNSceneDelegate {}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
// React Native 0.88's template, without its spaces: the scene delegate that makes React Native's factory and starts
|
|
25
|
+
// it in the scene's window.
|
|
26
|
+
const TEMPLATE = `import React
|
|
27
|
+
import React_RCTAppDelegate
|
|
28
|
+
import ReactAppDependencyProvider
|
|
29
|
+
import UIKit
|
|
30
|
+
|
|
31
|
+
class SceneDelegate: RCTDefaultReactNativeFactoryDelegate, UIWindowSceneDelegate {
|
|
32
|
+
var window: UIWindow?
|
|
33
|
+
var reactNativeFactory: RCTReactNativeFactory?
|
|
34
|
+
|
|
35
|
+
func scene(
|
|
36
|
+
_ scene: UIScene,
|
|
37
|
+
willConnectTo session: UISceneSession,
|
|
38
|
+
options connectionOptions: UIScene.ConnectionOptions
|
|
39
|
+
) {
|
|
40
|
+
guard let windowScene = scene as? UIWindowScene else {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dependencyProvider = RCTAppDependencyProvider()
|
|
45
|
+
reactNativeFactory = RCTReactNativeFactory(delegate: self)
|
|
46
|
+
window = UIWindow(windowScene: windowScene)
|
|
47
|
+
|
|
48
|
+
#if DEBUG
|
|
49
|
+
let devMenuConfiguration = RCTDevMenuConfiguration(
|
|
50
|
+
devMenuEnabled: true,
|
|
51
|
+
shakeGestureEnabled: true,
|
|
52
|
+
keyboardShortcutsEnabled: true
|
|
53
|
+
)
|
|
54
|
+
reactNativeFactory?.devMenuConfiguration = devMenuConfiguration
|
|
55
|
+
#endif
|
|
56
|
+
|
|
57
|
+
reactNativeFactory?.startReactNative(
|
|
58
|
+
withModuleName: "",
|
|
59
|
+
in: window,
|
|
60
|
+
connectionOptions: connectionOptions
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
65
|
+
RCTLinkingManager.scene(scene, openURLContexts: URLContexts)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
|
69
|
+
RCTLinkingManager.scene(scene, continue: userActivity)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override func bundleURL() -> URL? {
|
|
73
|
+
#if DEBUG
|
|
74
|
+
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
|
|
75
|
+
#else
|
|
76
|
+
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
|
77
|
+
#endif
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The text without its spaces and the app's name, to compare with the template.
|
|
84
|
+
* @param {string} text
|
|
85
|
+
*/
|
|
86
|
+
function shape(text) {
|
|
87
|
+
return text.replace(/withModuleName:\s*"[^"]*"/, 'withModuleName:""').replace(/\s+/g, '');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* SceneDelegate.swift of React Native 0.88 and newer: the scene delegate becomes an ANNSceneDelegate, since
|
|
92
|
+
* ANNAppDelegate starts React Native and owns the window. Only the template's file changes; one with code of the app's
|
|
93
|
+
* own is changed by hand.
|
|
94
|
+
* @param {string} text
|
|
95
|
+
* @returns {StepResult}
|
|
96
|
+
*/
|
|
97
|
+
function planSceneDelegate(text) {
|
|
98
|
+
if (
|
|
99
|
+
/class\s+\w+\s*:\s*ANNSceneDelegate\b/.test(text) ||
|
|
100
|
+
/\bApexNavigation\.attach\s*\(/.test(text)
|
|
101
|
+
)
|
|
102
|
+
return { status: 'done', note: 'already set up' };
|
|
103
|
+
if (shape(text) !== shape(TEMPLATE))
|
|
104
|
+
return { status: 'manual', note: 'changed by hand, see below', howTo: HOW_TO };
|
|
105
|
+
return { status: 'change', next: SET_UP, note: 'ANNSceneDelegate' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { planSceneDelegate };
|
|
@@ -78,7 +78,7 @@ navigation.push('Person', {
|
|
|
78
78
|
});
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
The avatar of the row moves and grows into the photo of the profile, and the name into its larger title. The views can differ in size and style. `type` and `duration` next to `shared` change how the rest of the screen comes, `'fade'` by default.
|
|
81
|
+
The avatar of the row moves and grows into the photo of the profile, and the name into its larger title. The views can differ in size and style. A pair that isn't found is skipped (the transition waits for it first); one that fails on the way is skipped with a warning in the log. Either way its views show with their screens, and the other pairs move. `type` and `duration` next to `shared` change how the rest of the screen comes, `'fade'` by default.
|
|
82
82
|
|
|
83
83
|
Both platforms. (Android) The next screen waits until it is drawn, so its views are there to move into; a `type` other than a fade gives way to a fade.
|
|
84
84
|
|
|
@@ -522,57 +522,39 @@ The launch link is read once per launch of the app, and the cases differ:
|
|
|
522
522
|
|
|
523
523
|
## An app with its own scene delegate
|
|
524
524
|
|
|
525
|
-
(iOS) `ANNAppDelegate` gives the app's scene `ANNSceneDelegate`, which moves the navigation's window into the scene and passes the scene's links on
|
|
525
|
+
(iOS) `ANNAppDelegate` gives the app's scene `ANNSceneDelegate`, which moves the navigation's window into the scene and passes the scene's links on: to the app delegate's methods, then to `Linking`. An app whose `Info.plist` names a scene delegate of its own, as React Native 0.88's template does, extends `ANNSceneDelegate`; a method it overrides calls `super`:
|
|
526
526
|
|
|
527
527
|
```swift
|
|
528
528
|
import ApexNavigation
|
|
529
529
|
import UIKit
|
|
530
530
|
|
|
531
|
-
class SceneDelegate:
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
func scene(
|
|
531
|
+
class SceneDelegate: ANNSceneDelegate {
|
|
532
|
+
override func scene(
|
|
535
533
|
_ scene: UIScene,
|
|
536
534
|
willConnectTo session: UISceneSession,
|
|
537
535
|
options connectionOptions: UIScene.ConnectionOptions
|
|
538
536
|
) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
for context in connectionOptions.urlContexts {
|
|
544
|
-
openLink(context.url)
|
|
545
|
-
}
|
|
546
|
-
for activity in connectionOptions.userActivities {
|
|
547
|
-
if let url = activity.webpageURL { openLink(url) }
|
|
548
|
-
}
|
|
537
|
+
// Moves the window into the scene and passes on the links
|
|
538
|
+
// that launched the app.
|
|
539
|
+
super.scene(scene, willConnectTo: session, options: connectionOptions)
|
|
540
|
+
window?.tintColor = .systemIndigo
|
|
549
541
|
}
|
|
550
542
|
|
|
551
|
-
|
|
552
|
-
func scene(
|
|
543
|
+
override func scene(
|
|
553
544
|
_ scene: UIScene,
|
|
554
545
|
openURLContexts URLContexts: Set<UIOpenURLContext>
|
|
555
546
|
) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
func scene(
|
|
562
|
-
_ scene: UIScene,
|
|
563
|
-
continue userActivity: NSUserActivity
|
|
564
|
-
) {
|
|
565
|
-
if let url = userActivity.webpageURL { openLink(url) }
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
private func openLink(_ url: URL) {
|
|
569
|
-
let app = UIApplication.shared.delegate as? ANNAppDelegate
|
|
570
|
-
app?.dispatchDeepLinkURL(url)
|
|
547
|
+
print("Opened:", URLContexts.map(\.url))
|
|
548
|
+
super.scene(scene, openURLContexts: URLContexts)
|
|
571
549
|
}
|
|
572
550
|
}
|
|
573
551
|
```
|
|
574
552
|
|
|
575
|
-
`
|
|
553
|
+
`super` passes each link on with its scene: React Native 0.88 and newer take a scene's links only with it. `setup` turns the scene delegate of React Native 0.88's template into `class SceneDelegate: ANNSceneDelegate {}`.
|
|
554
|
+
|
|
555
|
+
A scene delegate that extends another class calls `ApexNavigation.attach(to:)` (`attachToWindowScene:` in Objective-C) in `scene(_:willConnectTo:options:)` and passes the URLs to `dispatchDeepLinkURL(_:)` of the app delegate.
|
|
556
|
+
|
|
557
|
+
A link the app dispatches itself with `dispatchDeepLinkURL(_:)`, such as the link of a push notification, comes with no scene. Where React Native's `Linking` doesn't take it from the native side (React Native 0.88 and newer, in an app with a scene delegate), the navigation hands it to `Linking` from JavaScript, as a `url` event like any other.
|
|
576
558
|
|
|
577
559
|
## Testing
|
|
578
560
|
|
|
@@ -136,6 +136,7 @@ In an app as the React Native template makes it, the window shows **one React vi
|
|
|
136
136
|
| File | What setup does | Why |
|
|
137
137
|
| --- | --- | --- |
|
|
138
138
|
| `AppDelegate.swift` | Extends `ANNAppDelegate`. | It creates the window, puts the root of the navigation in it and starts React Native. Deep links that come before JS is ready wait for it. |
|
|
139
|
+
| `SceneDelegate.swift` | Extends `ANNSceneDelegate`. React Native 0.88 and newer only: the earlier templates have no scene delegate. | The template's scene delegate would start React Native a second time. `ANNSceneDelegate` moves the navigation's window into the app's scene and passes the scene's links to the app delegate. |
|
|
139
140
|
| `Info.plist` | Sets `UIViewControllerBasedStatusBarAppearance` to `true`. | Each screen sets its own status bar from its options; with `false`, only the app as a whole can. |
|
|
140
141
|
|
|
141
142
|
#### JavaScript files
|
|
@@ -133,19 +133,16 @@ static void ANNUpdateRootLayoutOf(UIViewController *node) {
|
|
|
133
133
|
_began = YES;
|
|
134
134
|
_wait = nil;
|
|
135
135
|
UIView *container = _context.containerView;
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
ANNTransitionViews *views = [[ANNTransitionViews alloc] initWithPairs:_plan.sharedElements
|
|
137
|
+
elements:_plan.elements
|
|
138
|
+
fromNode:_plan.fromNode
|
|
139
|
+
toNode:_plan.toNode];
|
|
138
140
|
NSMutableArray<id<ANNFrameClient>> *clients = [NSMutableArray new];
|
|
139
141
|
// The shared pairs first: their geometry and pictures are taken while the screens are still at rest.
|
|
140
142
|
NSArray<ANNSharedElementFlight *> *flights =
|
|
141
|
-
[ANNSharedElementFlight flightsOf:_plan.sharedElements
|
|
142
|
-
leavingRoot:leavingRoot
|
|
143
|
-
arrivingRoot:arrivingRoot
|
|
144
|
-
container:container];
|
|
143
|
+
[ANNSharedElementFlight flightsOf:_plan.sharedElements views:views container:container];
|
|
145
144
|
NSArray<ANNElementTransition *> *elements =
|
|
146
|
-
[ANNElementTransition transitionsOf:_plan.elements
|
|
147
|
-
leavingRoot:leavingRoot
|
|
148
|
-
arrivingRoot:arrivingRoot];
|
|
145
|
+
[ANNElementTransition transitionsOf:_plan.elements views:views];
|
|
149
146
|
ANNViewTracks *arrivingTracks = nil;
|
|
150
147
|
ANNViewTracks *leavingTracks = nil;
|
|
151
148
|
if (_plan.enter && _arriving) {
|
|
@@ -200,8 +197,13 @@ static void ANNUpdateRootLayoutOf(UIViewController *node) {
|
|
|
200
197
|
if (leavingTracks.hasTracks) {
|
|
201
198
|
[clients addObject:leavingTracks];
|
|
202
199
|
}
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
// Each element plays on its own: one whose frame fails goes back and stops, the others and the screens go on.
|
|
201
|
+
for (ANNElementTransition *element in elements) {
|
|
202
|
+
[clients addObject:[[ANNIsolatedElement alloc] initWithClient:element name:element.name]];
|
|
203
|
+
}
|
|
204
|
+
for (ANNSharedElementFlight *flight in flights) {
|
|
205
|
+
[clients addObject:[[ANNIsolatedElement alloc] initWithClient:flight name:flight.name]];
|
|
206
|
+
}
|
|
205
207
|
_clients = clients;
|
|
206
208
|
if ([self tilts]) {
|
|
207
209
|
[self orderInDepthIn:container flights:flights];
|
|
@@ -48,13 +48,12 @@ static const NSTimeInterval ANNReadyCheckDelay = 0.1;
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
- (BOOL)targetsAreFound {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
arrivingRoot:arrivingRoot];
|
|
51
|
+
ANNTransitionViews *views = [[ANNTransitionViews alloc] initWithPairs:_plan.sharedElements
|
|
52
|
+
elements:_plan.elements
|
|
53
|
+
fromNode:_plan.fromNode
|
|
54
|
+
toNode:_plan.toNode];
|
|
55
|
+
return [ANNSharedElementFlight allTargetsFound:_plan.sharedElements views:views] &&
|
|
56
|
+
[ANNElementTransition allFound:_plan.elements views:views];
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
- (BOOL)isReady {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
#import "ANNFrameDriver.h"
|
|
1
2
|
#import <UIKit/UIKit.h>
|
|
2
3
|
|
|
4
|
+
@class ANNElementTransitionOptions;
|
|
5
|
+
@class ANNSharedElementTransitionOptions;
|
|
6
|
+
@class Text;
|
|
7
|
+
|
|
3
8
|
NS_ASSUME_NONNULL_BEGIN
|
|
4
9
|
|
|
5
10
|
/// Finds the React views of the elements of a transition by their `nativeID`: depth first, the first match, in the
|
|
@@ -21,6 +26,58 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
21
26
|
+ (nullable UIView *)viewWithNativeID:(nullable NSString *)nativeID
|
|
22
27
|
inNode:(nullable UIViewController *)node;
|
|
23
28
|
|
|
29
|
+
/// The views with the `nativeID`s of `nativeIDs` in `root` and its subviews, in one walk: for each id the view
|
|
30
|
+
/// `viewWithNativeID:inView:` finds; an id no view has is left out. The walk stops once every id is found.
|
|
31
|
+
+ (NSDictionary<NSString *, UIView *> *)viewsWithNativeIDs:(NSSet<NSString *> *)nativeIDs
|
|
32
|
+
inView:(nullable UIView *)root;
|
|
33
|
+
|
|
34
|
+
@end
|
|
35
|
+
|
|
36
|
+
/// The views of a transition's elements in its two screens: each screen is walked once, at its first lookup, for the
|
|
37
|
+
/// ids looked for there — the leaving one for the pairs' `fromId`s and the elements' ids, the arriving one for the
|
|
38
|
+
/// pairs' `toId`s and the elements' ids the leaving one doesn't have. A lookup finds what
|
|
39
|
+
/// `+[ANNElementFinder viewWithNativeID:inView:]` finds in that screen as it was at its walk. Main queue.
|
|
40
|
+
@interface ANNTransitionViews : NSObject
|
|
41
|
+
|
|
42
|
+
- (instancetype)initWithPairs:(nullable NSArray<ANNSharedElementTransitionOptions *> *)pairs
|
|
43
|
+
elements:(nullable NSArray<ANNElementTransitionOptions *> *)elements
|
|
44
|
+
fromNode:(nullable UIViewController *)fromNode
|
|
45
|
+
toNode:(nullable UIViewController *)toNode NS_DESIGNATED_INITIALIZER;
|
|
46
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
47
|
+
+ (instancetype)new NS_UNAVAILABLE;
|
|
48
|
+
|
|
49
|
+
/// The React views of the screens the elements are looked for in.
|
|
50
|
+
@property(nonatomic, strong, readonly, nullable) UIView *leavingRoot;
|
|
51
|
+
@property(nonatomic, strong, readonly, nullable) UIView *arrivingRoot;
|
|
52
|
+
|
|
53
|
+
/// The view with `nativeID` in the leaving screen; nil when none has it or the id isn't one of the transition's.
|
|
54
|
+
- (nullable UIView *)leavingViewWithNativeID:(nullable NSString *)nativeID;
|
|
55
|
+
|
|
56
|
+
/// The view with `nativeID` in the arriving screen; nil when none has it or the id isn't one of the transition's.
|
|
57
|
+
- (nullable UIView *)arrivingViewWithNativeID:(nullable NSString *)nativeID;
|
|
58
|
+
|
|
59
|
+
@end
|
|
60
|
+
|
|
61
|
+
/// The `nativeID` an element's option holds; nil when it holds none (not a string, an empty one).
|
|
62
|
+
FOUNDATION_EXPORT NSString *_Nullable ANNNativeIDOf(Text *_Nullable value);
|
|
63
|
+
|
|
64
|
+
/// The isolation of every element of the core's transitions: `work`, a step of the element named `element` (its
|
|
65
|
+
/// `nativeID`, or a pair's two), runs; an exception in it skips that element only — one line in the log
|
|
66
|
+
/// (`ANNElementSkipped`), and NO — while the rest of the transition plays and its command completes.
|
|
67
|
+
FOUNDATION_EXPORT BOOL ANNElementStep(NSString *element, NS_NOESCAPE dispatch_block_t work);
|
|
68
|
+
|
|
69
|
+
/// The line in the log for the element named `element`, skipped for `reason`: it shows and hides with its screen.
|
|
70
|
+
FOUNDATION_EXPORT void ANNElementSkipped(NSString *element, NSString *reason);
|
|
71
|
+
|
|
72
|
+
/// An element's frames, each in `ANNElementStep`: after a frame that fails, the element's values go back
|
|
73
|
+
/// (`-restore`), and it gets no frame more.
|
|
74
|
+
@interface ANNIsolatedElement : NSObject <ANNFrameClient>
|
|
75
|
+
|
|
76
|
+
- (instancetype)initWithClient:(id<ANNFrameClient>)client
|
|
77
|
+
name:(NSString *)name NS_DESIGNATED_INITIALIZER;
|
|
78
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
79
|
+
+ (instancetype)new NS_UNAVAILABLE;
|
|
80
|
+
|
|
24
81
|
@end
|
|
25
82
|
|
|
26
83
|
NS_ASSUME_NONNULL_END
|
|
@@ -1,8 +1,58 @@
|
|
|
1
1
|
#import "ANNElementFinder.h"
|
|
2
2
|
#import "ANNAppearanceDispatcher.h"
|
|
3
|
+
#import "ANNElementTransitionOptions.h"
|
|
4
|
+
#import "ANNSharedElementTransitionOptions.h"
|
|
5
|
+
#import "Text.h"
|
|
3
6
|
#import "UIViewController+LayoutProtocol.h"
|
|
7
|
+
#import <React/RCTLog.h>
|
|
4
8
|
#import <React/RCTViewComponentView.h>
|
|
5
9
|
|
|
10
|
+
static NSString *ANNNativeIDOfView(UIView *view) {
|
|
11
|
+
return [view isKindOfClass:RCTViewComponentView.class]
|
|
12
|
+
? ((RCTViewComponentView *)view).nativeId
|
|
13
|
+
: nil;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Depth first, as the search by one id: the first view of the walk with an id keeps it. NO once every id is found.
|
|
17
|
+
static BOOL ANNCollectViews(UIView *view, NSSet<NSString *> *nativeIDs,
|
|
18
|
+
NSMutableDictionary<NSString *, UIView *> *found) {
|
|
19
|
+
NSString *nativeID = ANNNativeIDOfView(view);
|
|
20
|
+
if (nativeID && !found[nativeID] && [nativeIDs containsObject:nativeID]) {
|
|
21
|
+
found[nativeID] = view;
|
|
22
|
+
if (found.count == nativeIDs.count) {
|
|
23
|
+
return NO;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (UIView *subview in view.subviews) {
|
|
27
|
+
if (!ANNCollectViews(subview, nativeIDs, found)) {
|
|
28
|
+
return NO;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return YES;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
NSString *ANNNativeIDOf(Text *value) {
|
|
35
|
+
NSString *text = [value withDefault:nil];
|
|
36
|
+
return [text isKindOfClass:NSString.class] && text.length > 0 ? text : nil;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
void ANNElementSkipped(NSString *element, NSString *reason) {
|
|
40
|
+
RCTLogWarn(@"[ApexNavigation] the transition's element %@ is skipped, it shows and hides with "
|
|
41
|
+
@"its screen while the rest plays: %@",
|
|
42
|
+
element, reason);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
BOOL ANNElementStep(NSString *element, NS_NOESCAPE dispatch_block_t work) {
|
|
46
|
+
@try {
|
|
47
|
+
work();
|
|
48
|
+
return YES;
|
|
49
|
+
} @catch (NSException *exception) {
|
|
50
|
+
ANNElementSkipped(element, [NSString stringWithFormat:@"%@ (%@)", exception.reason ?: @"",
|
|
51
|
+
exception.name]);
|
|
52
|
+
return NO;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
6
56
|
@implementation ANNElementFinder
|
|
7
57
|
|
|
8
58
|
+ (UIView *)searchRootOf:(UIViewController *)node {
|
|
@@ -17,8 +67,7 @@
|
|
|
17
67
|
if (nativeID.length == 0 || !root) {
|
|
18
68
|
return nil;
|
|
19
69
|
}
|
|
20
|
-
if ([root
|
|
21
|
-
[((RCTViewComponentView *)root).nativeId isEqualToString:nativeID]) {
|
|
70
|
+
if ([ANNNativeIDOfView(root) isEqualToString:nativeID]) {
|
|
22
71
|
return root;
|
|
23
72
|
}
|
|
24
73
|
for (UIView *subview in root.subviews) {
|
|
@@ -34,4 +83,127 @@
|
|
|
34
83
|
return [self viewWithNativeID:nativeID inView:[self searchRootOf:node]];
|
|
35
84
|
}
|
|
36
85
|
|
|
86
|
+
+ (NSDictionary<NSString *, UIView *> *)viewsWithNativeIDs:(NSSet<NSString *> *)nativeIDs
|
|
87
|
+
inView:(UIView *)root {
|
|
88
|
+
NSMutableDictionary<NSString *, UIView *> *found = [NSMutableDictionary new];
|
|
89
|
+
if (root && nativeIDs.count > 0) {
|
|
90
|
+
ANNCollectViews(root, nativeIDs, found);
|
|
91
|
+
}
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@end
|
|
96
|
+
|
|
97
|
+
@implementation ANNTransitionViews {
|
|
98
|
+
NSSet<NSString *> *_leavingIDs;
|
|
99
|
+
NSSet<NSString *> *_targetIDs;
|
|
100
|
+
NSSet<NSString *> *_elementIDs;
|
|
101
|
+
NSDictionary<NSString *, UIView *> *_leavingViews;
|
|
102
|
+
NSDictionary<NSString *, UIView *> *_arrivingViews;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
- (instancetype)initWithPairs:(NSArray<ANNSharedElementTransitionOptions *> *)pairs
|
|
106
|
+
elements:(NSArray<ANNElementTransitionOptions *> *)elements
|
|
107
|
+
fromNode:(UIViewController *)fromNode
|
|
108
|
+
toNode:(UIViewController *)toNode {
|
|
109
|
+
if (self = [super init]) {
|
|
110
|
+
_leavingRoot = [ANNElementFinder searchRootOf:fromNode];
|
|
111
|
+
_arrivingRoot = [ANNElementFinder searchRootOf:toNode];
|
|
112
|
+
NSMutableSet<NSString *> *sources = [NSMutableSet new];
|
|
113
|
+
NSMutableSet<NSString *> *targets = [NSMutableSet new];
|
|
114
|
+
NSMutableSet<NSString *> *identifiers = [NSMutableSet new];
|
|
115
|
+
for (ANNSharedElementTransitionOptions *pair in pairs) {
|
|
116
|
+
NSString *fromId = ANNNativeIDOf(pair.fromId);
|
|
117
|
+
NSString *toId = ANNNativeIDOf(pair.toId);
|
|
118
|
+
if (fromId) {
|
|
119
|
+
[sources addObject:fromId];
|
|
120
|
+
}
|
|
121
|
+
if (toId) {
|
|
122
|
+
[targets addObject:toId];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (ANNElementTransitionOptions *element in elements) {
|
|
126
|
+
NSString *identifier = ANNNativeIDOf(element.identifier);
|
|
127
|
+
if (identifier) {
|
|
128
|
+
[identifiers addObject:identifier];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
[sources unionSet:identifiers];
|
|
132
|
+
_leavingIDs = sources;
|
|
133
|
+
_targetIDs = targets;
|
|
134
|
+
_elementIDs = identifiers;
|
|
135
|
+
}
|
|
136
|
+
return self;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
- (UIView *)leavingViewWithNativeID:(NSString *)nativeID {
|
|
140
|
+
if (nativeID.length == 0) {
|
|
141
|
+
return nil;
|
|
142
|
+
}
|
|
143
|
+
if (!_leavingViews) {
|
|
144
|
+
_leavingViews = [ANNElementFinder viewsWithNativeIDs:_leavingIDs inView:_leavingRoot];
|
|
145
|
+
}
|
|
146
|
+
return _leavingViews[nativeID];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
- (UIView *)arrivingViewWithNativeID:(NSString *)nativeID {
|
|
150
|
+
if (nativeID.length == 0) {
|
|
151
|
+
return nil;
|
|
152
|
+
}
|
|
153
|
+
if (!_arrivingViews) {
|
|
154
|
+
// An element is looked for in the arriving screen only when the leaving one doesn't have it: the walk stops
|
|
155
|
+
// sooner.
|
|
156
|
+
NSMutableSet<NSString *> *ids = [_targetIDs mutableCopy];
|
|
157
|
+
for (NSString *identifier in _elementIDs) {
|
|
158
|
+
if (![self leavingViewWithNativeID:identifier]) {
|
|
159
|
+
[ids addObject:identifier];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
_arrivingViews = [ANNElementFinder viewsWithNativeIDs:ids inView:_arrivingRoot];
|
|
163
|
+
}
|
|
164
|
+
return _arrivingViews[nativeID];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@end
|
|
168
|
+
|
|
169
|
+
@implementation ANNIsolatedElement {
|
|
170
|
+
id<ANNFrameClient> _client;
|
|
171
|
+
NSString *_name;
|
|
172
|
+
BOOL _failed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
- (instancetype)initWithClient:(id<ANNFrameClient>)client name:(NSString *)name {
|
|
176
|
+
if (self = [super init]) {
|
|
177
|
+
_client = client;
|
|
178
|
+
_name = [name copy];
|
|
179
|
+
}
|
|
180
|
+
return self;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#pragma mark - ANNFrameClient
|
|
184
|
+
|
|
185
|
+
- (void)applyAtTime:(NSTimeInterval)time {
|
|
186
|
+
if (_failed) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
id<ANNFrameClient> client = _client;
|
|
190
|
+
if (!ANNElementStep(_name, ^{
|
|
191
|
+
[client applyAtTime:time];
|
|
192
|
+
})) {
|
|
193
|
+
_failed = YES;
|
|
194
|
+
[self restore];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
- (NSTimeInterval)endTime {
|
|
199
|
+
return _client.endTime;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
- (void)restore {
|
|
203
|
+
id<ANNFrameClient> client = _client;
|
|
204
|
+
ANNElementStep(_name, ^{
|
|
205
|
+
[client restore];
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
37
209
|
@end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#import <UIKit/UIKit.h>
|
|
3
3
|
|
|
4
4
|
@class ANNElementTransitionOptions;
|
|
5
|
+
@class ANNTransitionViews;
|
|
5
6
|
|
|
6
7
|
NS_ASSUME_NONNULL_BEGIN
|
|
7
8
|
|
|
@@ -11,20 +12,21 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
11
12
|
@interface ANNElementTransition : NSObject <ANNFrameClient>
|
|
12
13
|
|
|
13
14
|
/// The items found: each `id` first in the leaving screen's React view, then in the arriving one; an item not found or
|
|
14
|
-
/// without an animating property is left out.
|
|
15
|
+
/// without an animating property is left out, and so is one whose start fails (`ANNElementStep`).
|
|
15
16
|
+ (NSArray<ANNElementTransition *> *)
|
|
16
17
|
transitionsOf:(nullable NSArray<ANNElementTransitionOptions *> *)elements
|
|
17
|
-
|
|
18
|
-
arrivingRoot:(nullable UIView *)arrivingRoot;
|
|
18
|
+
views:(ANNTransitionViews *)views;
|
|
19
19
|
|
|
20
20
|
/// Every item's `id` is found in one of the two screens (the wait's test).
|
|
21
21
|
+ (BOOL)allFound:(nullable NSArray<ANNElementTransitionOptions *> *)elements
|
|
22
|
-
|
|
23
|
-
arrivingRoot:(nullable UIView *)arrivingRoot;
|
|
22
|
+
views:(ANNTransitionViews *)views;
|
|
24
23
|
|
|
25
24
|
- (instancetype)init NS_UNAVAILABLE;
|
|
26
25
|
+ (instancetype)new NS_UNAVAILABLE;
|
|
27
26
|
|
|
27
|
+
/// The item's `id`, in quotes: its name in the log.
|
|
28
|
+
@property(nonatomic, copy, readonly) NSString *name;
|
|
29
|
+
|
|
28
30
|
@end
|
|
29
31
|
|
|
30
32
|
NS_ASSUME_NONNULL_END
|
|
@@ -7,51 +7,49 @@
|
|
|
7
7
|
ANNViewTracks *_tracks;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
- (instancetype)initWithTracks:(ANNViewTracks *)tracks {
|
|
10
|
+
- (instancetype)initWithTracks:(ANNViewTracks *)tracks name:(NSString *)name {
|
|
11
11
|
if (self = [super init]) {
|
|
12
12
|
_tracks = tracks;
|
|
13
|
+
_name = [name copy];
|
|
13
14
|
}
|
|
14
15
|
return self;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
+ (UIView *)viewOf:(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
NSString *identifier = [element.identifier withDefault:nil];
|
|
21
|
-
if (![identifier isKindOfClass:NSString.class] || identifier.length == 0) {
|
|
22
|
-
return nil;
|
|
23
|
-
}
|
|
24
|
-
return [ANNElementFinder viewWithNativeID:identifier inView:leavingRoot]
|
|
25
|
-
?: [ANNElementFinder viewWithNativeID:identifier inView:arrivingRoot];
|
|
18
|
+
+ (UIView *)viewOf:(NSString *)identifier views:(ANNTransitionViews *)views {
|
|
19
|
+
return [views leavingViewWithNativeID:identifier]
|
|
20
|
+
?: [views arrivingViewWithNativeID:identifier];
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
+ (NSArray<ANNElementTransition *> *)
|
|
29
24
|
transitionsOf:(NSArray<ANNElementTransitionOptions *> *)elements
|
|
30
|
-
|
|
31
|
-
arrivingRoot:(UIView *)arrivingRoot {
|
|
25
|
+
views:(ANNTransitionViews *)views {
|
|
32
26
|
NSMutableArray<ANNElementTransition *> *transitions = [NSMutableArray new];
|
|
33
27
|
// One view takes one item: a second item for the same view would read the values the first one writes.
|
|
34
28
|
NSHashTable<UIView *> *taken = [NSHashTable weakObjectsHashTable];
|
|
35
29
|
for (ANNElementTransitionOptions *element in elements) {
|
|
36
|
-
|
|
30
|
+
NSString *identifier = ANNNativeIDOf(element.identifier);
|
|
31
|
+
UIView *view = [self viewOf:identifier views:views];
|
|
37
32
|
if (!view || [taken containsObject:view]) {
|
|
38
33
|
continue;
|
|
39
34
|
}
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
NSString *name = [NSString stringWithFormat:@"'%@'", identifier];
|
|
36
|
+
__block ANNViewTracks *tracks = nil;
|
|
37
|
+
if (!ANNElementStep(name, ^{
|
|
38
|
+
tracks = [[ANNViewTracks alloc] initWithView:view element:element];
|
|
39
|
+
}) ||
|
|
40
|
+
!tracks.hasTracks) {
|
|
42
41
|
continue;
|
|
43
42
|
}
|
|
44
43
|
[taken addObject:view];
|
|
45
|
-
[transitions addObject:[[ANNElementTransition alloc] initWithTracks:tracks]];
|
|
44
|
+
[transitions addObject:[[ANNElementTransition alloc] initWithTracks:tracks name:name]];
|
|
46
45
|
}
|
|
47
46
|
return transitions;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
+ (BOOL)allFound:(NSArray<ANNElementTransitionOptions *> *)elements
|
|
51
|
-
|
|
52
|
-
arrivingRoot:(UIView *)arrivingRoot {
|
|
50
|
+
views:(ANNTransitionViews *)views {
|
|
53
51
|
for (ANNElementTransitionOptions *element in elements) {
|
|
54
|
-
if (![self viewOf:element
|
|
52
|
+
if (![self viewOf:ANNNativeIDOf(element.identifier) views:views]) {
|
|
55
53
|
return NO;
|
|
56
54
|
}
|
|
57
55
|
}
|