@ajuarezso/capacitor-liquid-glass 0.3.0 → 0.3.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.
@@ -23,63 +23,119 @@ struct LiquidGlassTabItem {
23
23
 
24
24
  /// Visual style of the tab bar background.
25
25
  enum LiquidGlassTabBarStyle: String {
26
- /// Translucent blurred — auto-applies Liquid Glass on iOS 26+ SDK.
26
+ /// Translucent blurred — adopta Liquid Glass automáticamente en iOS 26+
27
+ /// cuando el binario está linkeado contra el SDK iOS 26 (Xcode 26+).
27
28
  case `default`
28
- /// Minimal blur (`systemUltraThinMaterial`) — more see-through than default.
29
+ /// Minimal blur (`systemUltraThinMaterial`) — más ver-through que default.
30
+ /// NOTA: este override CANCELA el adopt automático de Liquid Glass.
29
31
  case ultraThin
30
- /// No background, no blur — content behind shows through 100%.
32
+ /// No background, no blur — content behind se ve completo.
33
+ /// NOTA: este override CANCELA el adopt automático de Liquid Glass.
31
34
  case transparent
32
- /// REAL iOS 26 `UIGlassEffect` (Music + App Store material).
33
- /// Fallback en iOS < 26 `UIBlurEffect.systemThinMaterial`.
35
+ /// Liquid Glass nativo iOS 26 alias funcional de `.default`. Se mantiene
36
+ /// como case separado para semántica explícita en el consumer ("este
37
+ /// layout pide específicamente Liquid Glass, no fallback genérico").
34
38
  case liquidGlass
35
39
  }
36
40
 
37
- /// A floating UITabBar overlay that adopts iOS 26 Liquid Glass automatically
38
- /// when the app is built against the iOS 26 SDK. On earlier iOS the bar falls
39
- /// back to the translucent blurred UITabBar appearance.
40
- final class LiquidGlassTabBarOverlay: NSObject {
41
+ /// Tab bar flotante que adopta iOS 26 Liquid Glass automáticamente cuando la
42
+ /// app se compila contra el SDK iOS 26.
43
+ ///
44
+ /// **Decisión arquitectónica crítica**: este view controller se adjunta como
45
+ /// CHILD del rootViewController del window, y el `UITabBar` vive como subview
46
+ /// de `self.view`. Esto crea el view controller hierarchy completo que iOS 26
47
+ /// necesita para aplicar el material Liquid Glass automáticamente al UITabBar.
48
+ ///
49
+ /// Agregar el `UITabBar` directamente al `UIWindow` (sin view controller
50
+ /// container) NO funciona — iOS 26 omite el adopt automático y la pill se
51
+ /// renderiza con un material translúcido genérico que se ve opaco en
52
+ /// comparación con el material real de Music / App Store.
53
+ ///
54
+ /// Patrón inspirado en `stay-liquid` (alistairheath/stay-liquid GitHub).
55
+ final class LiquidGlassTabBarOverlay: UIViewController {
41
56
 
42
57
  // Public callbacks
43
58
  var onTabSelected: ((Int, String) -> Void)?
44
59
  var onLayoutChanged: ((Double, Double) -> Void)?
45
60
 
46
61
  // Internal state
47
- private weak var window: UIWindow?
48
- private var tabBar: UITabBar?
62
+ private let tabBar = UITabBar()
49
63
  private var items: [LiquidGlassTabItem] = []
64
+ private weak var hostVC: UIViewController?
50
65
 
51
- func attach(to window: UIWindow) {
52
- if self.window === window, tabBar != nil { return }
53
- self.window = window
66
+ override func viewDidLoad() {
67
+ super.viewDidLoad()
68
+ // Background transparente — solo el `UITabBar` (la pill flotante)
69
+ // ocupa espacio visible. El resto del área es transparente y `hitTest`
70
+ // la deja pasar al webview por debajo (ver override más abajo).
71
+ view.backgroundColor = .clear
54
72
 
55
- let tabBar = UITabBar()
56
73
  tabBar.translatesAutoresizingMaskIntoConstraints = false
57
74
  tabBar.delegate = self
58
75
 
59
76
  // Default appearance: triggers Liquid Glass automatically on iOS 26+.
60
- // (Sobreescrito en `configure(...)` si el caller pidió otro estilo).
77
+ // Sobreescrito en `configure(...)` si el caller pidió otro estilo.
61
78
  applyAppearance(tabBar, style: .default)
62
79
 
63
- window.addSubview(tabBar)
80
+ view.addSubview(tabBar)
64
81
 
65
82
  NSLayoutConstraint.activate([
66
- tabBar.leadingAnchor.constraint(equalTo: window.leadingAnchor),
67
- tabBar.trailingAnchor.constraint(equalTo: window.trailingAnchor),
68
- tabBar.bottomAnchor.constraint(equalTo: window.bottomAnchor),
83
+ tabBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
84
+ tabBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
85
+ tabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
69
86
  ])
87
+ }
88
+
89
+ /// `self.view` cubre todo el rootVC (constraints leading/trailing/top/
90
+ /// bottom = rootVC.view). Sin este override absorbería TODOS los taps y
91
+ /// bloquearía el webview de Capacitor. Solo dejamos pasar los toques que
92
+ /// aterrizan sobre el `UITabBar` o sus subvistas.
93
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
94
+ let hit = super.hitTest(point, with: event)
95
+ guard let hit else { return nil }
96
+ if hit === tabBar || hit.isDescendant(of: tabBar) { return hit }
97
+ return nil
98
+ }
99
+
100
+ /// Adjunta este view controller como child del rootViewController del
101
+ /// window. Idempotente: si ya está adjuntado al mismo root, no-op.
102
+ func attach(to window: UIWindow) {
103
+ guard let rootVC = window.rootViewController else { return }
104
+ if self.parent === rootVC { return }
105
+ // Si está adjuntado a otro VC (cambio de root entre sesiones),
106
+ // detachar primero antes de re-adjuntar.
107
+ if self.parent != nil {
108
+ self.willMove(toParent: nil)
109
+ self.view.removeFromSuperview()
110
+ self.removeFromParent()
111
+ }
70
112
 
71
- self.tabBar = tabBar
113
+ rootVC.addChild(self)
114
+ self.view.translatesAutoresizingMaskIntoConstraints = false
115
+ rootVC.view.addSubview(self.view)
116
+ NSLayoutConstraint.activate([
117
+ self.view.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
118
+ self.view.trailingAnchor.constraint(equalTo: rootVC.view.trailingAnchor),
119
+ self.view.topAnchor.constraint(equalTo: rootVC.view.topAnchor),
120
+ self.view.bottomAnchor.constraint(equalTo: rootVC.view.bottomAnchor),
121
+ ])
122
+ self.didMove(toParent: rootVC)
123
+ self.hostVC = rootVC
72
124
  emitLayout()
73
125
  }
74
126
 
75
127
  /// Aplica el appearance correspondiente al estilo solicitado.
76
- /// - `default`: Liquid Glass (iOS 26+) o blur translúcido como fallback.
128
+ /// - `default` / `liquidGlass`: Liquid Glass nativo iOS 26 (SDK 26 + runtime
129
+ /// 26). En iOS < 26 cae al blur translúcido legacy.
77
130
  /// - `ultraThin`: blur mínimo `systemUltraThinMaterial` — más ver-through.
78
131
  /// - `transparent`: sin background ni blur — content behind se ve completo.
79
132
  private func applyAppearance(_ tabBar: UITabBar, style: LiquidGlassTabBarStyle) {
80
133
  let appearance = UITabBarAppearance()
81
134
  switch style {
82
- case .default:
135
+ case .default, .liquidGlass:
136
+ // En iOS 26+ con SDK 26, esto adopta Liquid Glass automáticamente.
137
+ // NO hacer override de `backgroundEffect` — eso anula el adopt y
138
+ // produce un material opaco genérico.
83
139
  appearance.configureWithDefaultBackground()
84
140
  case .ultraThin:
85
141
  appearance.configureWithDefaultBackground()
@@ -90,23 +146,6 @@ final class LiquidGlassTabBarOverlay: NSObject {
90
146
  appearance.backgroundEffect = nil
91
147
  appearance.backgroundColor = UIColor.clear
92
148
  appearance.shadowColor = UIColor.clear
93
- case .liquidGlass:
94
- // REAL iOS 26 Liquid Glass — el mismo material que usan Music y
95
- // App Store. `UIGlassEffect` solo está disponible compilando con
96
- // el SDK iOS 26+ (Xcode 26+). En iOS < 26 runtime cae a
97
- // `systemThinMaterial`, que es la mejor aproximación legacy con
98
- // vibrancy del sistema.
99
- appearance.configureWithDefaultBackground()
100
- #if compiler(>=6.1)
101
- if #available(iOS 26.0, *) {
102
- appearance.backgroundEffect = UIGlassEffect()
103
- } else {
104
- appearance.backgroundEffect = UIBlurEffect(style: .systemThinMaterial)
105
- }
106
- #else
107
- appearance.backgroundEffect = UIBlurEffect(style: .systemThinMaterial)
108
- #endif
109
- appearance.backgroundColor = UIColor.clear
110
149
  }
111
150
  tabBar.standardAppearance = appearance
112
151
  if #available(iOS 15.0, *) {
@@ -115,8 +154,6 @@ final class LiquidGlassTabBarOverlay: NSObject {
115
154
  }
116
155
 
117
156
  func configure(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?, style: LiquidGlassTabBarStyle) {
118
- guard let tabBar else { return }
119
-
120
157
  // Preserve selection across re-configs (badge changes, etc.)
121
158
  let previousSelectedId: String? = tabBar.selectedItem.flatMap { current in
122
159
  self.items.indices.contains(current.tag) ? self.items[current.tag].id : nil
@@ -141,8 +178,6 @@ final class LiquidGlassTabBarOverlay: NSObject {
141
178
 
142
179
  tabBar.setItems(uiItems, animated: false)
143
180
 
144
- // Si el caller pidió explícitamente "no selection" (selectedIndex < 0),
145
- // respetar eso y NO restaurar la selección previa — deja selectedItem = nil.
146
181
  if selectedIndex < 0 {
147
182
  tabBar.selectedItem = nil
148
183
  } else if let previousSelectedId, let restoreIdx = items.firstIndex(where: { $0.id == previousSelectedId }) {
@@ -158,7 +193,7 @@ final class LiquidGlassTabBarOverlay: NSObject {
158
193
  }
159
194
 
160
195
  func updateBadge(id: String, badge: String?) {
161
- guard let tabBar, let tabBarItems = tabBar.items,
196
+ guard let tabBarItems = tabBar.items,
162
197
  let idx = items.firstIndex(where: { $0.id == id }),
163
198
  idx < tabBarItems.count else { return }
164
199
  let value = (badge?.isEmpty ?? true) ? nil : badge
@@ -166,14 +201,10 @@ final class LiquidGlassTabBarOverlay: NSObject {
166
201
  }
167
202
 
168
203
  func show() {
169
- guard let tabBar = tabBar else { return }
170
- // Si ya está visible, no hacer nada (evita flicker en re-show consecutivos)
171
204
  if !tabBar.isHidden && tabBar.alpha == 1.0 { emitLayout(); return }
172
- // Reset estado pre-animación
173
205
  tabBar.isHidden = false
174
206
  tabBar.alpha = 0
175
207
  tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
176
- // Fade-in + slide-up (curva native iOS para apariciones)
177
208
  UIView.animate(
178
209
  withDuration: 0.28,
179
210
  delay: 0,
@@ -181,8 +212,8 @@ final class LiquidGlassTabBarOverlay: NSObject {
181
212
  initialSpringVelocity: 0,
182
213
  options: [.curveEaseOut, .allowUserInteraction],
183
214
  animations: {
184
- tabBar.alpha = 1
185
- tabBar.transform = .identity
215
+ self.tabBar.alpha = 1
216
+ self.tabBar.transform = .identity
186
217
  },
187
218
  completion: nil
188
219
  )
@@ -190,27 +221,24 @@ final class LiquidGlassTabBarOverlay: NSObject {
190
221
  }
191
222
 
192
223
  func hide() {
193
- guard let tabBar = tabBar else { return }
194
224
  if tabBar.isHidden { return }
195
- // Fade-out + slide-down (más rápido que el show — el exit es snappy)
196
225
  UIView.animate(
197
226
  withDuration: 0.18,
198
227
  delay: 0,
199
228
  options: [.curveEaseIn, .allowUserInteraction],
200
229
  animations: {
201
- tabBar.alpha = 0
202
- tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
230
+ self.tabBar.alpha = 0
231
+ self.tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
203
232
  },
204
233
  completion: { _ in
205
- tabBar.isHidden = true
206
- tabBar.transform = .identity
234
+ self.tabBar.isHidden = true
235
+ self.tabBar.transform = .identity
207
236
  }
208
237
  )
209
238
  }
210
239
 
211
240
  func setSelectedIndex(_ index: Int) {
212
- guard let tabBar, let items = tabBar.items else { return }
213
- // index < 0 → deseleccionar todos los tabs (selectedItem = nil)
241
+ guard let items = tabBar.items else { return }
214
242
  if index < 0 {
215
243
  tabBar.selectedItem = nil
216
244
  return
@@ -225,7 +253,6 @@ final class LiquidGlassTabBarOverlay: NSObject {
225
253
  }
226
254
 
227
255
  func currentLayout() -> (height: Double, bottomSafeArea: Double) {
228
- guard let tabBar else { return (0, 0) }
229
256
  tabBar.layoutIfNeeded()
230
257
  let bottomInset = tabBar.safeAreaInsets.bottom
231
258
  return (Double(tabBar.frame.height), Double(bottomInset))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ajuarezso/capacitor-liquid-glass",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "iOS 26 Liquid Glass native chrome (TabBar, NavigationBar, Alerts, Sheets) for Capacitor apps. Falls back gracefully on iOS < 26 and Android.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",