|
|
@@ -8,15 +8,70 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
|
|
|
var window: UIWindow?
|
|
|
var navController: UINavigationController?
|
|
|
+
|
|
|
+ // ---------------------------------------------------------
|
|
|
+ // MARK: Multi-stack routing state
|
|
|
+ // ---------------------------------------------------------
|
|
|
+
|
|
|
+ /// Currently active logical stack id (must match CLJS :stack, e.g. "home", "capture", "goto").
|
|
|
+ private var activeStackId: String = "home"
|
|
|
+
|
|
|
+ /// Per-stack path stacks, including the active one.
|
|
|
+ /// Example: ["home": ["/", "/page/A"], "capture": ["/__stack__/capture"]]
|
|
|
+ private var stackPathStacks: [String: [String]] = [
|
|
|
+ "home": ["/"]
|
|
|
+ ]
|
|
|
+
|
|
|
+ /// Mirror of the active stack's paths.
|
|
|
private var pathStack: [String] = ["/"]
|
|
|
- private var ignoreRoutePopCount = 0
|
|
|
+
|
|
|
+ /// Used to ignore JS-driven pops when we're popping in response to a native gesture.
|
|
|
+ private var ignoreRoutePopCount: Int = 0
|
|
|
+
|
|
|
+ /// Temporary snapshot image for smooth pop transitions.
|
|
|
private var popSnapshotView: UIView?
|
|
|
|
|
|
+ // ---------------------------------------------------------
|
|
|
+ // MARK: Helpers
|
|
|
+ // ---------------------------------------------------------
|
|
|
+
|
|
|
private func normalizedPath(_ raw: String?) -> String {
|
|
|
guard let raw = raw, !raw.isEmpty else { return "/" }
|
|
|
return raw
|
|
|
}
|
|
|
|
|
|
+ private func debugLogStacks(_ label: String) {
|
|
|
+ #if DEBUG
|
|
|
+ print("🧭 [\(label)] activeStackId=\(activeStackId)")
|
|
|
+ print(" pathStack=\(pathStack)")
|
|
|
+ print(" stackPathStacks=\(stackPathStacks)")
|
|
|
+ #endif
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Returns the current native path stack for a given logical stack id,
|
|
|
+ /// or initialises a sensible default if none exists yet.
|
|
|
+ private func paths(for stackId: String) -> [String] {
|
|
|
+ if let existing = stackPathStacks[stackId], !existing.isEmpty {
|
|
|
+ return existing
|
|
|
+ }
|
|
|
+
|
|
|
+ if stackId == "home" {
|
|
|
+ return ["/"]
|
|
|
+ } else {
|
|
|
+ // Virtual stacks (e.g. capture, search, goto) default to a stack-root path.
|
|
|
+ return ["/__stack__/\(stackId)"]
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Updates the stored paths for a given stack id and keeps `pathStack`
|
|
|
+ /// consistent if this is the active stack.
|
|
|
+ private func setPaths(_ paths: [String], for stackId: String) {
|
|
|
+ stackPathStacks[stackId] = paths
|
|
|
+ if stackId == activeStackId {
|
|
|
+ pathStack = paths
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// ---------------------------------------------------------
|
|
|
// MARK: UIApplication lifecycle
|
|
|
// ---------------------------------------------------------
|
|
|
@@ -124,7 +179,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------
|
|
|
- // MARK: Navigation operations
|
|
|
+ // MARK: Navigation operations (within active stack)
|
|
|
// ---------------------------------------------------------
|
|
|
|
|
|
private func emptyNavStack(path: String) {
|
|
|
@@ -137,10 +192,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
|
|
|
let vc = NativePageViewController(path: path, push: false)
|
|
|
pathStack = [path]
|
|
|
+ setPaths(pathStack, for: activeStackId)
|
|
|
|
|
|
nav.setViewControllers([vc], animated: false)
|
|
|
SharedWebViewController.instance.clearPlaceholder()
|
|
|
SharedWebViewController.instance.attach(to: vc)
|
|
|
+
|
|
|
+ debugLogStacks("emptyNavStack")
|
|
|
}
|
|
|
|
|
|
private func pushIfNeeded(path: String, animated: Bool) {
|
|
|
@@ -154,7 +212,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
|
|
|
let vc = NativePageViewController(path: path, push: true)
|
|
|
pathStack.append(path)
|
|
|
+ setPaths(pathStack, for: activeStackId)
|
|
|
+
|
|
|
nav.pushViewController(vc, animated: animated)
|
|
|
+
|
|
|
+ debugLogStacks("pushIfNeeded")
|
|
|
}
|
|
|
|
|
|
private func replaceTop(path: String) {
|
|
|
@@ -162,9 +224,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
guard let nav = navController else { return }
|
|
|
|
|
|
_ = pathStack.popLast()
|
|
|
- let vc = NativePageViewController(path: path, push: false)
|
|
|
pathStack.append(path)
|
|
|
+ setPaths(pathStack, for: activeStackId)
|
|
|
|
|
|
+ let vc = NativePageViewController(path: path, push: false)
|
|
|
var stack = nav.viewControllers
|
|
|
if stack.isEmpty {
|
|
|
stack = [vc]
|
|
|
@@ -172,6 +235,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
stack[stack.count - 1] = vc
|
|
|
}
|
|
|
nav.setViewControllers(stack, animated: false)
|
|
|
+
|
|
|
+ debugLogStacks("replaceTop")
|
|
|
}
|
|
|
|
|
|
private func popIfNeeded(animated: Bool) {
|
|
|
@@ -179,7 +244,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
|
|
|
if nav.viewControllers.count > 1 {
|
|
|
_ = pathStack.popLast()
|
|
|
+ setPaths(pathStack, for: activeStackId)
|
|
|
nav.popViewController(animated: animated)
|
|
|
+
|
|
|
+ debugLogStacks("popIfNeeded")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -206,15 +274,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
vcs.count < pathStack.count
|
|
|
}
|
|
|
|
|
|
+ #if DEBUG
|
|
|
+ print("🧭 willShow — isPop=\(isPop)")
|
|
|
+ print(" toVC=\(toVC.targetPath) fromVC=\(String(describing: fromVC?.targetPath))")
|
|
|
+ debugLogStacks("willShow")
|
|
|
+ #endif
|
|
|
+
|
|
|
if isPop {
|
|
|
// -----------------------------
|
|
|
- // POP — keep your existing logic
|
|
|
+ // POP — update per-stack pathStack, then notify JS.
|
|
|
// -----------------------------
|
|
|
let previousStack = pathStack
|
|
|
- if pathStack.count > 1 { _ = pathStack.popLast() }
|
|
|
+
|
|
|
+ if pathStack.count > 1 {
|
|
|
+ _ = pathStack.popLast()
|
|
|
+ }
|
|
|
if let last = pathStack.last, last != toVC.targetPath {
|
|
|
pathStack[pathStack.count - 1] = toVC.targetPath
|
|
|
}
|
|
|
+ setPaths(pathStack, for: activeStackId)
|
|
|
|
|
|
popSnapshotView?.removeFromSuperview()
|
|
|
popSnapshotView = nil
|
|
|
@@ -227,9 +305,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
popSnapshotView = iv
|
|
|
}
|
|
|
|
|
|
- coordinator.animate(alongsideTransition: nil) { ctx in
|
|
|
+ coordinator.animate(alongsideTransition: nil) { [weak self] ctx in
|
|
|
+ guard let self else { return }
|
|
|
+
|
|
|
guard !ctx.isCancelled else {
|
|
|
self.pathStack = previousStack
|
|
|
+ self.setPaths(previousStack, for: self.activeStackId)
|
|
|
+
|
|
|
if let fromVC {
|
|
|
SharedWebViewController.instance.attach(to: fromVC)
|
|
|
}
|
|
|
@@ -237,12 +319,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if let webView = SharedWebViewController.instance.bridgeController.bridge?.webView,
|
|
|
- webView.canGoBack {
|
|
|
- self.ignoreRoutePopCount += 1
|
|
|
- webView.goBack()
|
|
|
- } else {
|
|
|
- self.ignoreRoutePopCount += 1
|
|
|
+ // 🔑 DO NOT call webView.goBack().
|
|
|
+ // Tell JS explicitly that native popped.
|
|
|
+ self.ignoreRoutePopCount += 1
|
|
|
+ #if DEBUG
|
|
|
+ print("⬅️ Native POP completed, notifying JS via onNativePop(), ignoreRoutePopCount=\(self.ignoreRoutePopCount)")
|
|
|
+ debugLogStacks("after native-pop pathStack update")
|
|
|
+ #endif
|
|
|
+
|
|
|
+ if let bridge = SharedWebViewController.instance.bridgeController.bridge {
|
|
|
+ let js = "window.LogseqNative && window.LogseqNative.onNativePop && window.LogseqNative.onNativePop();"
|
|
|
+ bridge.webView?.evaluateJavaScript(js, completionHandler: nil)
|
|
|
}
|
|
|
|
|
|
SharedWebViewController.instance.attach(
|
|
|
@@ -264,8 +351,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
// -----------------------------
|
|
|
// PUSH / RESET
|
|
|
// -----------------------------
|
|
|
- // Attach the shared webview to the *destination* page
|
|
|
- // before/during the animation so it can start rendering immediately.
|
|
|
SharedWebViewController.instance.attach(
|
|
|
to: toVC,
|
|
|
leavePlaceholderInPreviousParent: fromVC != nil
|
|
|
@@ -273,10 +358,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
|
|
|
coordinator.animate(alongsideTransition: nil) { ctx in
|
|
|
if ctx.isCancelled, let fromVC {
|
|
|
- // If the push is cancelled (interactive back), put the webview back.
|
|
|
SharedWebViewController.instance.attach(to: fromVC)
|
|
|
} else {
|
|
|
- // Transition completed → clear any placeholders.
|
|
|
SharedWebViewController.instance.clearPlaceholder()
|
|
|
}
|
|
|
}
|
|
|
@@ -294,7 +377,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
SharedWebViewController.instance.clearPlaceholder()
|
|
|
SharedWebViewController.instance.attach(to: current)
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
|
|
|
func navigationController(
|
|
|
@@ -308,7 +390,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------
|
|
|
- // MARK: Route Observation
|
|
|
+ // MARK: Route Observation (JS -> Native)
|
|
|
// ---------------------------------------------------------
|
|
|
|
|
|
private func observeRouteChanges() {
|
|
|
@@ -318,10 +400,69 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
queue: .main
|
|
|
) { [weak self] notification in
|
|
|
guard let self else { return }
|
|
|
+ guard let nav = self.navController else { return }
|
|
|
|
|
|
- let path = self.normalizedPath(notification.userInfo?["path"] as? String)
|
|
|
+ let rawPath = notification.userInfo?["path"] as? String
|
|
|
+ let path = self.normalizedPath(rawPath)
|
|
|
let navigationType = (notification.userInfo?["navigationType"] as? String) ?? "push"
|
|
|
+ let stackId = (notification.userInfo?["stack"] as? String) ?? "home"
|
|
|
+
|
|
|
+ #if DEBUG
|
|
|
+ print("📡 routeDidChange from JS → native")
|
|
|
+ print(" stackId=\(stackId) navigationType=\(navigationType) path=\(path)")
|
|
|
+ debugLogStacks("before observeRouteChanges")
|
|
|
+ #endif
|
|
|
+
|
|
|
+ // ============================================
|
|
|
+ // 1️⃣ Stack switch: home ↔ capture ↔ goto ...
|
|
|
+ // ============================================
|
|
|
+ if stackId != self.activeStackId {
|
|
|
+ // Save current native stack paths
|
|
|
+ self.setPaths(self.pathStack, for: self.activeStackId)
|
|
|
+
|
|
|
+ // Load (or create) new stack's paths
|
|
|
+ var newPaths = self.paths(for: stackId)
|
|
|
+
|
|
|
+ // Ensure the top of the stack matches the path sent by JS
|
|
|
+ if let last = newPaths.last, last != path {
|
|
|
+ if newPaths.isEmpty {
|
|
|
+ newPaths = [path]
|
|
|
+ } else {
|
|
|
+ newPaths[newPaths.count - 1] = path
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ self.activeStackId = stackId
|
|
|
+ self.pathStack = newPaths
|
|
|
+ self.setPaths(newPaths, for: stackId)
|
|
|
|
|
|
+ // Rebuild the UINavigationController's stack from these paths
|
|
|
+ var vcs: [UIViewController] = []
|
|
|
+ for (idx, p) in newPaths.enumerated() {
|
|
|
+ let vc = NativePageViewController(path: p, push: idx > 0)
|
|
|
+ vcs.append(vc)
|
|
|
+ }
|
|
|
+
|
|
|
+ nav.setViewControllers(vcs, animated: false)
|
|
|
+
|
|
|
+ if let lastVC = vcs.last as? NativePageViewController {
|
|
|
+ SharedWebViewController.instance.attach(to: lastVC)
|
|
|
+ SharedWebViewController.instance.clearPlaceholder()
|
|
|
+ }
|
|
|
+
|
|
|
+ #if DEBUG
|
|
|
+ print("🔀 STACK SWITCH to \(stackId)")
|
|
|
+ debugLogStacks("after stack switch")
|
|
|
+ #endif
|
|
|
+
|
|
|
+ // For stacks like "capture", default paths are ["__/stack__/capture"],
|
|
|
+ // so they get a single VC and no back button.
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================================
|
|
|
+ // 2️⃣ Navigation *within* the active stack
|
|
|
+ // ============================================
|
|
|
switch navigationType {
|
|
|
case "reset":
|
|
|
self.emptyNavStack(path: path)
|
|
|
@@ -332,9 +473,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
case "pop":
|
|
|
if self.ignoreRoutePopCount > 0 {
|
|
|
self.ignoreRoutePopCount -= 1
|
|
|
+ #if DEBUG
|
|
|
+ print("🙈 ignoring JS pop (ignoreRoutePopCount→\(self.ignoreRoutePopCount))")
|
|
|
+ debugLogStacks("after ignore JS pop")
|
|
|
+ #endif
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
if self.pathStack.count > 1 {
|
|
|
self.popIfNeeded(animated: true)
|
|
|
}
|
|
|
@@ -342,6 +486,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
|
|
default:
|
|
|
self.pushIfNeeded(path: path, animated: true)
|
|
|
}
|
|
|
+
|
|
|
+ #if DEBUG
|
|
|
+ debugLogStacks("after observeRouteChanges switch")
|
|
|
+ #endif
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -372,6 +520,10 @@ extension NSUserActivity {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// ---------------------------------------------------------
|
|
|
+// MARK: Convenience
|
|
|
+// ---------------------------------------------------------
|
|
|
+
|
|
|
extension AppDelegate {
|
|
|
func donateQuickAddShortcut() {
|
|
|
let a = NSUserActivity.quickAdd
|