@@ -8,15 +8,70 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
88
99 var window : UIWindow ?
1010 var navController : UINavigationController ?
11+
12+ // ---------------------------------------------------------
13+ // MARK: Multi-stack routing state
14+ // ---------------------------------------------------------
15+
16+ /// Currently active logical stack id (must match CLJS :stack, e.g. "home", "capture", "goto").
17+ private var activeStackId : String = " home "
18+
19+ /// Per-stack path stacks, including the active one.
20+ /// Example: ["home": ["/", "/page/A"], "capture": ["/__stack__/capture"]]
21+ private var stackPathStacks : [ String : [ String ] ] = [
22+ " home " : [ " / " ]
23+ ]
24+
25+ /// Mirror of the active stack's paths.
1126 private var pathStack : [ String ] = [ " / " ]
12- private var ignoreRoutePopCount = 0
27+
28+ /// Used to ignore JS-driven pops when we're popping in response to a native gesture.
29+ private var ignoreRoutePopCount : Int = 0
30+
31+ /// Temporary snapshot image for smooth pop transitions.
1332 private var popSnapshotView : UIView ?
1433
34+ // ---------------------------------------------------------
35+ // MARK: Helpers
36+ // ---------------------------------------------------------
37+
1538 private func normalizedPath( _ raw: String ? ) -> String {
1639 guard let raw = raw, !raw. isEmpty else { return " / " }
1740 return raw
1841 }
1942
43+ private func debugLogStacks( _ label: String ) {
44+ #if DEBUG
45+ print ( " 🧭 [ \( label) ] activeStackId= \( activeStackId) " )
46+ print ( " pathStack= \( pathStack) " )
47+ print ( " stackPathStacks= \( stackPathStacks) " )
48+ #endif
49+ }
50+
51+ /// Returns the current native path stack for a given logical stack id,
52+ /// or initialises a sensible default if none exists yet.
53+ private func paths( for stackId: String ) -> [ String ] {
54+ if let existing = stackPathStacks [ stackId] , !existing. isEmpty {
55+ return existing
56+ }
57+
58+ if stackId == " home " {
59+ return [ " / " ]
60+ } else {
61+ // Virtual stacks (e.g. capture, search, goto) default to a stack-root path.
62+ return [ " /__stack__/ \( stackId) " ]
63+ }
64+ }
65+
66+ /// Updates the stored paths for a given stack id and keeps `pathStack`
67+ /// consistent if this is the active stack.
68+ private func setPaths( _ paths: [ String ] , for stackId: String ) {
69+ stackPathStacks [ stackId] = paths
70+ if stackId == activeStackId {
71+ pathStack = paths
72+ }
73+ }
74+
2075 // ---------------------------------------------------------
2176 // MARK: UIApplication lifecycle
2277 // ---------------------------------------------------------
@@ -124,7 +179,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
124179 }
125180
126181 // ---------------------------------------------------------
127- // MARK: Navigation operations
182+ // MARK: Navigation operations (within active stack)
128183 // ---------------------------------------------------------
129184
130185 private func emptyNavStack( path: String ) {
@@ -137,10 +192,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
137192
138193 let vc = NativePageViewController ( path: path, push: false )
139194 pathStack = [ path]
195+ setPaths ( pathStack, for: activeStackId)
140196
141197 nav. setViewControllers ( [ vc] , animated: false )
142198 SharedWebViewController . instance. clearPlaceholder ( )
143199 SharedWebViewController . instance. attach ( to: vc)
200+
201+ debugLogStacks ( " emptyNavStack " )
144202 }
145203
146204 private func pushIfNeeded( path: String , animated: Bool ) {
@@ -154,32 +212,42 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
154212
155213 let vc = NativePageViewController ( path: path, push: true )
156214 pathStack. append ( path)
215+ setPaths ( pathStack, for: activeStackId)
216+
157217 nav. pushViewController ( vc, animated: animated)
218+
219+ debugLogStacks ( " pushIfNeeded " )
158220 }
159221
160222 private func replaceTop( path: String ) {
161223 let path = normalizedPath ( path)
162224 guard let nav = navController else { return }
163225
164226 _ = pathStack. popLast ( )
165- let vc = NativePageViewController ( path: path, push: false )
166227 pathStack. append ( path)
228+ setPaths ( pathStack, for: activeStackId)
167229
230+ let vc = NativePageViewController ( path: path, push: false )
168231 var stack = nav. viewControllers
169232 if stack. isEmpty {
170233 stack = [ vc]
171234 } else {
172235 stack [ stack. count - 1 ] = vc
173236 }
174237 nav. setViewControllers ( stack, animated: false )
238+
239+ debugLogStacks ( " replaceTop " )
175240 }
176241
177242 private func popIfNeeded( animated: Bool ) {
178243 guard let nav = navController else { return }
179244
180245 if nav. viewControllers. count > 1 {
181246 _ = pathStack. popLast ( )
247+ setPaths ( pathStack, for: activeStackId)
182248 nav. popViewController ( animated: animated)
249+
250+ debugLogStacks ( " popIfNeeded " )
183251 }
184252 }
185253
@@ -206,15 +274,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
206274 vcs. count < pathStack. count
207275 }
208276
277+ #if DEBUG
278+ print ( " 🧭 willShow — isPop= \( isPop) " )
279+ print ( " toVC= \( toVC. targetPath) fromVC= \( String ( describing: fromVC? . targetPath) ) " )
280+ debugLogStacks ( " willShow " )
281+ #endif
282+
209283 if isPop {
210284 // -----------------------------
211- // POP — keep your existing logic
285+ // POP — update per-stack pathStack, then notify JS.
212286 // -----------------------------
213287 let previousStack = pathStack
214- if pathStack. count > 1 { _ = pathStack. popLast ( ) }
288+
289+ if pathStack. count > 1 {
290+ _ = pathStack. popLast ( )
291+ }
215292 if let last = pathStack. last, last != toVC. targetPath {
216293 pathStack [ pathStack. count - 1 ] = toVC. targetPath
217294 }
295+ setPaths ( pathStack, for: activeStackId)
218296
219297 popSnapshotView? . removeFromSuperview ( )
220298 popSnapshotView = nil
@@ -227,22 +305,31 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
227305 popSnapshotView = iv
228306 }
229307
230- coordinator. animate ( alongsideTransition: nil ) { ctx in
308+ coordinator. animate ( alongsideTransition: nil ) { [ weak self] ctx in
309+ guard let self else { return }
310+
231311 guard !ctx. isCancelled else {
232312 self . pathStack = previousStack
313+ self . setPaths ( previousStack, for: self . activeStackId)
314+
233315 if let fromVC {
234316 SharedWebViewController . instance. attach ( to: fromVC)
235317 }
236318 SharedWebViewController . instance. clearPlaceholder ( )
237319 return
238320 }
239321
240- if let webView = SharedWebViewController . instance. bridgeController. bridge? . webView,
241- webView. canGoBack {
242- self . ignoreRoutePopCount += 1
243- webView. goBack ( )
244- } else {
245- self . ignoreRoutePopCount += 1
322+ // 🔑 DO NOT call webView.goBack().
323+ // Tell JS explicitly that native popped.
324+ self . ignoreRoutePopCount += 1
325+ #if DEBUG
326+ print ( " ⬅️ Native POP completed, notifying JS via onNativePop(), ignoreRoutePopCount= \( self . ignoreRoutePopCount) " )
327+ debugLogStacks ( " after native-pop pathStack update " )
328+ #endif
329+
330+ if let bridge = SharedWebViewController . instance. bridgeController. bridge {
331+ let js = " window.LogseqNative && window.LogseqNative.onNativePop && window.LogseqNative.onNativePop(); "
332+ bridge. webView? . evaluateJavaScript ( js, completionHandler: nil )
246333 }
247334
248335 SharedWebViewController . instance. attach (
@@ -264,19 +351,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
264351 // -----------------------------
265352 // PUSH / RESET
266353 // -----------------------------
267- // Attach the shared webview to the *destination* page
268- // before/during the animation so it can start rendering immediately.
269354 SharedWebViewController . instance. attach (
270355 to: toVC,
271356 leavePlaceholderInPreviousParent: fromVC != nil
272357 )
273358
274359 coordinator. animate ( alongsideTransition: nil ) { ctx in
275360 if ctx. isCancelled, let fromVC {
276- // If the push is cancelled (interactive back), put the webview back.
277361 SharedWebViewController . instance. attach ( to: fromVC)
278362 } else {
279- // Transition completed → clear any placeholders.
280363 SharedWebViewController . instance. clearPlaceholder ( )
281364 }
282365 }
@@ -294,7 +377,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
294377 SharedWebViewController . instance. clearPlaceholder ( )
295378 SharedWebViewController . instance. attach ( to: current)
296379 }
297-
298380 }
299381
300382 func navigationController(
@@ -308,7 +390,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
308390 }
309391
310392 // ---------------------------------------------------------
311- // MARK: Route Observation
393+ // MARK: Route Observation (JS -> Native)
312394 // ---------------------------------------------------------
313395
314396 private func observeRouteChanges( ) {
@@ -318,10 +400,69 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
318400 queue: . main
319401 ) { [ weak self] notification in
320402 guard let self else { return }
403+ guard let nav = self . navController else { return }
321404
322- let path = self . normalizedPath ( notification. userInfo ? [ " path " ] as? String )
405+ let rawPath = notification. userInfo ? [ " path " ] as? String
406+ let path = self . normalizedPath ( rawPath)
323407 let navigationType = ( notification. userInfo ? [ " navigationType " ] as? String ) ?? " push "
408+ let stackId = ( notification. userInfo ? [ " stack " ] as? String ) ?? " home "
409+
410+ #if DEBUG
411+ print ( " 📡 routeDidChange from JS → native " )
412+ print ( " stackId= \( stackId) navigationType= \( navigationType) path= \( path) " )
413+ debugLogStacks ( " before observeRouteChanges " )
414+ #endif
415+
416+ // ============================================
417+ // 1️⃣ Stack switch: home ↔ capture ↔ goto ...
418+ // ============================================
419+ if stackId != self . activeStackId {
420+ // Save current native stack paths
421+ self . setPaths ( self . pathStack, for: self . activeStackId)
422+
423+ // Load (or create) new stack's paths
424+ var newPaths = self . paths ( for: stackId)
425+
426+ // Ensure the top of the stack matches the path sent by JS
427+ if let last = newPaths. last, last != path {
428+ if newPaths. isEmpty {
429+ newPaths = [ path]
430+ } else {
431+ newPaths [ newPaths. count - 1 ] = path
432+ }
433+ }
434+
435+ self . activeStackId = stackId
436+ self . pathStack = newPaths
437+ self . setPaths ( newPaths, for: stackId)
324438
439+ // Rebuild the UINavigationController's stack from these paths
440+ var vcs : [ UIViewController ] = [ ]
441+ for (idx, p) in newPaths. enumerated ( ) {
442+ let vc = NativePageViewController ( path: p, push: idx > 0 )
443+ vcs. append ( vc)
444+ }
445+
446+ nav. setViewControllers ( vcs, animated: false )
447+
448+ if let lastVC = vcs. last as? NativePageViewController {
449+ SharedWebViewController . instance. attach ( to: lastVC)
450+ SharedWebViewController . instance. clearPlaceholder ( )
451+ }
452+
453+ #if DEBUG
454+ print ( " 🔀 STACK SWITCH to \( stackId) " )
455+ debugLogStacks ( " after stack switch " )
456+ #endif
457+
458+ // For stacks like "capture", default paths are ["__/stack__/capture"],
459+ // so they get a single VC and no back button.
460+ return
461+ }
462+
463+ // ============================================
464+ // 2️⃣ Navigation *within* the active stack
465+ // ============================================
325466 switch navigationType {
326467 case " reset " :
327468 self . emptyNavStack ( path: path)
@@ -332,16 +473,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
332473 case " pop " :
333474 if self . ignoreRoutePopCount > 0 {
334475 self . ignoreRoutePopCount -= 1
476+ #if DEBUG
477+ print ( " 🙈 ignoring JS pop (ignoreRoutePopCount→ \( self . ignoreRoutePopCount) ) " )
478+ debugLogStacks ( " after ignore JS pop " )
479+ #endif
335480 return
336481 }
337-
338482 if self . pathStack. count > 1 {
339483 self . popIfNeeded ( animated: true )
340484 }
341485
342486 default :
343487 self . pushIfNeeded ( path: path, animated: true )
344488 }
489+
490+ #if DEBUG
491+ debugLogStacks ( " after observeRouteChanges switch " )
492+ #endif
345493 }
346494 }
347495}
@@ -372,6 +520,10 @@ extension NSUserActivity {
372520 }
373521}
374522
523+ // ---------------------------------------------------------
524+ // MARK: Convenience
525+ // ---------------------------------------------------------
526+
375527extension AppDelegate {
376528 func donateQuickAddShortcut( ) {
377529 let a = NSUserActivity . quickAdd
0 commit comments