@capgo/capacitor-native-navigation 8.0.15 → 8.0.16

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.
@@ -44,6 +44,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
44
44
  private var tabBar: UITabBar?
45
45
  private var tabBarController: NativeNavigationTabController?
46
46
  private var tabViewControllers: [UIViewController] = []
47
+ private weak var systemTabRootContainer: UIView?
47
48
  private weak var originalWebViewSuperview: UIView?
48
49
  private var originalWebViewIndex: Int?
49
50
  private var originalWebViewAutoresizingMask: UIView.AutoresizingMask?
@@ -437,6 +438,13 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
437
438
  notifyTabSelect(index: item.tag)
438
439
  }
439
440
 
441
+ public func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
442
+ if usesSystemLiquidGlass {
443
+ hostWebView(in: viewController)
444
+ }
445
+ return true
446
+ }
447
+
440
448
  public func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
441
449
  guard !suppressTabSelectEvent else {
442
450
  hostWebViewInSelectedSystemTab()
@@ -552,8 +560,9 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
552
560
  controller.view.isHidden = !tabbarVisible
553
561
 
554
562
  if let parent = bridge?.viewController {
563
+ let containerView = systemTabHostingContainerView(in: parent)
555
564
  parent.addChild(controller)
556
- insertSystemTabControllerView(controller.view, in: parent.view)
565
+ insertSystemTabControllerView(controller.view, in: containerView)
557
566
  controller.didMove(toParent: parent)
558
567
  }
559
568
 
@@ -563,32 +572,94 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
563
572
  return controller.tabBar
564
573
  }
565
574
 
575
+ private func systemTabHostingContainerView(in parent: UIViewController) -> UIView {
576
+ if let systemTabRootContainer = systemTabRootContainer {
577
+ return systemTabRootContainer
578
+ }
579
+
580
+ guard let webView = webView,
581
+ parent.view === webView else {
582
+ return parent.view
583
+ }
584
+
585
+ let previousSuperview = webView.superview
586
+ let previousIndex = previousSuperview?.subviews.firstIndex(of: webView)
587
+ let previousFrame = webView.frame
588
+ let previousAutoresizingMask = webView.autoresizingMask
589
+ let container = UIView(frame: previousFrame)
590
+ container.backgroundColor = nativeNavigationFallbackBackground(for: webView)
591
+ container.isOpaque = true
592
+ container.autoresizingMask = previousAutoresizingMask.isEmpty ? [.flexibleWidth, .flexibleHeight] : previousAutoresizingMask
593
+
594
+ if let previousSuperview = previousSuperview {
595
+ previousSuperview.insertSubview(container, at: min(previousIndex ?? previousSuperview.subviews.count, previousSuperview.subviews.count))
596
+ }
597
+
598
+ parent.view = container
599
+ container.addSubview(webView)
600
+ webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
601
+ webView.frame = container.bounds
602
+ moveNativeChrome(from: webView, to: container)
603
+
604
+ systemTabRootContainer = container
605
+ originalWebViewSuperview = container
606
+ originalWebViewIndex = 0
607
+ originalWebViewAutoresizingMask = webView.autoresizingMask
608
+ return container
609
+ }
610
+
611
+ private func moveNativeChrome(from webView: UIView, to container: UIView) {
612
+ if let navContainer = navContainer,
613
+ navContainer.superview === webView {
614
+ container.addSubview(navContainer)
615
+ }
616
+ }
617
+
566
618
  private func applySystemTabBarItems(_ items: [UITabBarItem], selectedIndex: Int?, animated: Bool) {
567
619
  guard let tabBarController = tabBarController else {
568
620
  return
569
621
  }
570
622
 
571
623
  let previousSelectedIndex = tabBarController.selectedIndex
572
- let controllers = items.map { item -> UIViewController in
573
- let controller = NativeNavigationTabContentController()
574
- controller.tabBarItem = item
575
- return controller
576
- }
624
+ let controllers = systemTabContentControllers(for: items)
625
+ let currentControllers = tabBarController.viewControllers ?? []
626
+ let shouldUpdateControllers = currentControllers.count != controllers.count
627
+ || zip(currentControllers, controllers).contains { currentController, nextController in
628
+ currentController !== nextController
629
+ }
577
630
  let shouldAnimate = animated && tabBarController.viewControllers?.count == controllers.count
578
631
 
579
632
  suppressTabSelectEvent = true
580
- tabBarController.setViewControllers(controllers, animated: shouldAnimate)
633
+ if shouldUpdateControllers {
634
+ tabBarController.setViewControllers(controllers, animated: shouldAnimate)
635
+ }
581
636
  if !controllers.isEmpty {
582
637
  let fallbackIndex = selectedIndex ?? previousSelectedIndex
583
638
  let index = min(max(fallbackIndex, 0), controllers.count - 1)
639
+ hostWebView(in: controllers[index])
584
640
  tabBarController.selectedIndex = index
585
641
  }
586
- hostWebViewInSelectedSystemTab()
587
642
  suppressTabSelectEvent = false
588
643
 
589
644
  tabViewControllers = controllers
590
645
  }
591
646
 
647
+ private func systemTabContentControllers(for items: [UITabBarItem]) -> [UIViewController] {
648
+ let existingControllers = tabViewControllers.compactMap { $0 as? NativeNavigationTabContentController }
649
+ if existingControllers.count == items.count {
650
+ zip(existingControllers, items).forEach { controller, item in
651
+ controller.tabBarItem = item
652
+ }
653
+ return existingControllers
654
+ }
655
+
656
+ return items.map { item -> UIViewController in
657
+ let controller = NativeNavigationTabContentController()
658
+ controller.tabBarItem = item
659
+ return controller
660
+ }
661
+ }
662
+
592
663
  private func setSystemTabBarHidden(_ hidden: Bool) {
593
664
  guard let tabBarController = tabBarController else {
594
665
  return
@@ -671,15 +742,22 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
671
742
  }
672
743
 
673
744
  private func hostWebViewInSelectedSystemTab() {
745
+ hostWebView(in: tabBarController?.selectedViewController)
746
+ }
747
+
748
+ private func hostWebView(in viewController: UIViewController?) {
674
749
  guard usesSystemLiquidGlass,
675
750
  let webView = webView,
676
- let selectedController = tabBarController?.selectedViewController as? NativeNavigationTabContentController else {
751
+ let selectedController = viewController as? NativeNavigationTabContentController else {
677
752
  return
678
753
  }
679
754
 
680
755
  captureOriginalWebViewPlacementIfNeeded(webView)
681
- clearHostedWebViews(matching: webView, except: selectedController)
682
- selectedController.host(webView: webView)
756
+ clearHostedWebViews(matching: webView, except: selectedController, preservingSnapshots: true)
757
+ guard selectedController.host(webView: webView) else {
758
+ isWebViewHostedInSystemTabController = false
759
+ return
760
+ }
683
761
  clearHostedWebViews(matching: webView, except: selectedController)
684
762
  isWebViewHostedInSystemTabController = true
685
763
  }
@@ -700,11 +778,15 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
700
778
  isWebViewHostedInSystemTabController = false
701
779
  }
702
780
 
703
- private func clearHostedWebViews(matching webView: UIView, except owner: NativeNavigationTabContentController? = nil) {
781
+ private func clearHostedWebViews(
782
+ matching webView: UIView,
783
+ except owner: NativeNavigationTabContentController? = nil,
784
+ preservingSnapshots: Bool = false
785
+ ) {
704
786
  tabViewControllers
705
787
  .compactMap { $0 as? NativeNavigationTabContentController }
706
788
  .filter { $0 !== owner }
707
- .forEach { $0.clearHostedWebView(ifMatching: webView) }
789
+ .forEach { $0.clearHostedWebView(ifMatching: webView, preservingSnapshot: preservingSnapshots) }
708
790
  }
709
791
 
710
792
  private func makeBarButtonItems(_ rawItems: [[String: Any]], placement: String) -> [UIBarButtonItem] {
@@ -900,12 +982,12 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
900
982
 
901
983
  private func transitionSnapshotView(from webView: UIView, sourceRect: CGRect?) -> UIView {
902
984
  guard let sourceRect = sourceRect else {
903
- return webView.snapshotView(afterScreenUpdates: false) ?? UIView(frame: webView.frame)
985
+ return webView.snapshotView(afterScreenUpdates: false) ?? nativeNavigationSnapshotPlaceholder(for: webView)
904
986
  }
905
987
 
906
988
  let cropRect = sourceRect.intersection(webView.bounds)
907
989
  guard cropRect.width > 0, cropRect.height > 0 else {
908
- return webView.snapshotView(afterScreenUpdates: false) ?? UIView(frame: webView.frame)
990
+ return webView.snapshotView(afterScreenUpdates: false) ?? nativeNavigationSnapshotPlaceholder(for: webView)
909
991
  }
910
992
 
911
993
  let renderer = UIGraphicsImageRenderer(bounds: webView.bounds)
@@ -921,7 +1003,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
921
1003
  ).integral
922
1004
 
923
1005
  guard let croppedImage = image.cgImage?.cropping(to: scaledCropRect) else {
924
- return webView.snapshotView(afterScreenUpdates: false) ?? UIView(frame: webView.frame)
1006
+ return webView.snapshotView(afterScreenUpdates: false) ?? nativeNavigationSnapshotPlaceholder(for: webView)
925
1007
  }
926
1008
 
927
1009
  let imageView = UIImageView(image: UIImage(cgImage: croppedImage, scale: scale, orientation: image.imageOrientation))
@@ -1367,22 +1449,23 @@ private final class NativeNavigationBar: UINavigationBar {
1367
1449
  }
1368
1450
  }
1369
1451
 
1370
- private final class NativeNavigationTabController: UITabBarController {
1452
+ final class NativeNavigationTabController: UITabBarController {
1371
1453
  override func viewDidLoad() {
1372
1454
  super.viewDidLoad()
1373
- view.backgroundColor = .clear
1374
- view.isOpaque = false
1455
+ view.backgroundColor = .systemBackground
1456
+ view.isOpaque = true
1375
1457
  tabBar.isTranslucent = true
1376
1458
  }
1377
1459
  }
1378
1460
 
1379
- private final class NativeNavigationTabContentController: UIViewController {
1461
+ final class NativeNavigationTabContentController: UIViewController {
1380
1462
  private weak var hostedWebView: UIView?
1463
+ private var snapshotPlaceholder: UIView?
1381
1464
 
1382
1465
  override func loadView() {
1383
1466
  let view = UIView()
1384
- view.backgroundColor = .clear
1385
- view.isOpaque = false
1467
+ view.backgroundColor = .systemBackground
1468
+ view.isOpaque = true
1386
1469
  self.view = view
1387
1470
  }
1388
1471
 
@@ -1395,24 +1478,56 @@ private final class NativeNavigationTabContentController: UIViewController {
1395
1478
  hostedWebView?.frame = view.bounds
1396
1479
  }
1397
1480
 
1398
- func clearHostedWebView(ifMatching webView: UIView? = nil) {
1481
+ func clearHostedWebView(ifMatching webView: UIView? = nil, preservingSnapshot: Bool = false) {
1399
1482
  guard webView == nil || hostedWebView === webView else {
1400
1483
  return
1401
1484
  }
1485
+
1486
+ if preservingSnapshot, let hostedWebView = hostedWebView, hostedWebView.superview === view {
1487
+ let placeholder = hostedWebView.snapshotView(afterScreenUpdates: false) ?? nativeNavigationSnapshotPlaceholder(for: hostedWebView)
1488
+ placeholder.frame = hostedWebView.frame
1489
+ placeholder.autoresizingMask = [.flexibleWidth, .flexibleHeight]
1490
+ snapshotPlaceholder?.removeFromSuperview()
1491
+ view.insertSubview(placeholder, belowSubview: hostedWebView)
1492
+ snapshotPlaceholder = placeholder
1493
+ }
1494
+
1402
1495
  hostedWebView = nil
1403
1496
  }
1404
1497
 
1405
- func host(webView: UIView) {
1406
- if hostedWebView !== webView {
1407
- hostedWebView = webView
1498
+ @discardableResult
1499
+ func host(webView: UIView) -> Bool {
1500
+ guard view !== webView, !view.isDescendant(of: webView) else {
1501
+ hostedWebView = nil
1502
+ return false
1408
1503
  }
1504
+
1505
+ snapshotPlaceholder?.removeFromSuperview()
1506
+ snapshotPlaceholder = nil
1507
+ hostedWebView = webView
1409
1508
  if webView.superview !== view {
1410
1509
  webView.removeFromSuperview()
1411
1510
  view.addSubview(webView)
1412
1511
  }
1413
1512
  webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
1414
1513
  webView.frame = view.bounds
1514
+ return true
1515
+ }
1516
+ }
1517
+
1518
+ private func nativeNavigationFallbackBackground(for view: UIView) -> UIColor {
1519
+ if let color = view.backgroundColor,
1520
+ color.cgColor.alpha > 0 {
1521
+ return color
1415
1522
  }
1523
+ return .systemBackground
1524
+ }
1525
+
1526
+ private func nativeNavigationSnapshotPlaceholder(for view: UIView) -> UIView {
1527
+ let placeholder = UIView(frame: view.frame)
1528
+ placeholder.backgroundColor = nativeNavigationFallbackBackground(for: view)
1529
+ placeholder.isOpaque = true
1530
+ return placeholder
1416
1531
  }
1417
1532
 
1418
1533
  private final class SVGIconRenderer: NSObject, XMLParserDelegate {
@@ -1,19 +1,65 @@
1
1
  import XCTest
2
+ import UIKit
2
3
  @testable import NativeNavigationPlugin
3
4
 
4
5
  class NativeNavigationTests: XCTestCase {
5
- func testEcho() {
6
- let implementation = NativeNavigation()
7
- let value = "Hello, World!"
8
- let result = implementation.echo(value)
9
-
10
- XCTAssertEqual(value, result)
11
- }
12
-
13
6
  func testGetPluginVersion() {
14
7
  let implementation = NativeNavigation()
15
8
  let result = implementation.getPluginVersion()
16
9
 
17
10
  XCTAssertEqual("native", result)
18
11
  }
12
+
13
+ func testTabContentControllerHostsWebView() {
14
+ let webView = UIView()
15
+ let originalContainer = UIView()
16
+ let controller = NativeNavigationTabContentController()
17
+ _ = controller.view
18
+
19
+ originalContainer.addSubview(webView)
20
+
21
+ XCTAssertTrue(controller.host(webView: webView))
22
+ XCTAssertEqual(webView.superview, controller.view)
23
+ XCTAssertEqual(webView.frame, controller.view.bounds)
24
+ }
25
+
26
+ func testTabContentControllerKeepsSnapshotPlaceholderWhenWebViewMoves() {
27
+ let webView = UIView()
28
+ let firstController = NativeNavigationTabContentController()
29
+ let secondController = NativeNavigationTabContentController()
30
+ _ = firstController.view
31
+ _ = secondController.view
32
+
33
+ firstController.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
34
+ secondController.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
35
+ webView.backgroundColor = .systemBackground
36
+
37
+ XCTAssertTrue(firstController.host(webView: webView))
38
+ XCTAssertEqual(firstController.view.subviews.count, 1)
39
+
40
+ firstController.clearHostedWebView(ifMatching: webView, preservingSnapshot: true)
41
+ XCTAssertEqual(firstController.view.subviews.count, 2)
42
+
43
+ XCTAssertTrue(secondController.host(webView: webView))
44
+ XCTAssertEqual(webView.superview, secondController.view)
45
+ XCTAssertEqual(firstController.view.subviews.count, 1)
46
+ XCTAssertFalse(firstController.view.subviews.contains(webView))
47
+
48
+ XCTAssertTrue(firstController.host(webView: webView))
49
+ XCTAssertEqual(webView.superview, firstController.view)
50
+ XCTAssertEqual(firstController.view.subviews.count, 1)
51
+ XCTAssertTrue(firstController.view.subviews.first === webView)
52
+ }
53
+
54
+ func testTabContentControllerRejectsLayerCycle() {
55
+ let webView = UIView()
56
+ let controller = NativeNavigationTabContentController()
57
+ _ = controller.view
58
+
59
+ webView.addSubview(controller.view)
60
+
61
+ XCTAssertFalse(controller.host(webView: webView))
62
+ XCTAssertNil(webView.superview)
63
+ XCTAssertEqual(controller.view.superview, webView)
64
+ }
19
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-native-navigation",
3
- "version": "8.0.15",
3
+ "version": "8.0.16",
4
4
  "description": "Capacitor plugin for native navbar, tabbar, and route transition chrome over a WebView.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -26,6 +26,9 @@
26
26
  "url": "https://github.com/Cap-go/capacitor-native-navigation/issues"
27
27
  },
28
28
  "homepage": "https://capgo.app/docs/plugins/native-navigation/",
29
+ "engines": {
30
+ "node": ">=22.0.0"
31
+ },
29
32
  "keywords": [
30
33
  "capacitor",
31
34
  "native-navigation",