@ajuarezso/capacitor-liquid-glass 0.3.1 → 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,94 +23,129 @@ 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] = []
50
- /// Overlay `UIVisualEffectView` que provee el material cuando el style es
51
- /// `.liquidGlass`. Solo se monta cuando el estilo lo requiere, y se
52
- /// destruye al cambiar a otro estilo. Vive como subview del window justo
53
- /// DEBAJO del `tabBar` (que en este modo queda 100% transparente).
54
- private var glassOverlayView: UIVisualEffectView?
64
+ private weak var hostVC: UIViewController?
55
65
 
56
- func attach(to window: UIWindow) {
57
- if self.window === window, tabBar != nil { return }
58
- 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
59
72
 
60
- let tabBar = UITabBar()
61
73
  tabBar.translatesAutoresizingMaskIntoConstraints = false
62
74
  tabBar.delegate = self
63
75
 
64
76
  // Default appearance: triggers Liquid Glass automatically on iOS 26+.
65
- // (Sobreescrito en `configure(...)` si el caller pidió otro estilo).
77
+ // Sobreescrito en `configure(...)` si el caller pidió otro estilo.
66
78
  applyAppearance(tabBar, style: .default)
67
79
 
68
- window.addSubview(tabBar)
80
+ view.addSubview(tabBar)
69
81
 
70
82
  NSLayoutConstraint.activate([
71
- tabBar.leadingAnchor.constraint(equalTo: window.leadingAnchor),
72
- tabBar.trailingAnchor.constraint(equalTo: window.trailingAnchor),
73
- 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),
74
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
+ }
75
112
 
76
- 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
77
124
  emitLayout()
78
125
  }
79
126
 
80
127
  /// Aplica el appearance correspondiente al estilo solicitado.
81
- /// - `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.
82
130
  /// - `ultraThin`: blur mínimo `systemUltraThinMaterial` — más ver-through.
83
131
  /// - `transparent`: sin background ni blur — content behind se ve completo.
84
- /// - `liquidGlass`: tab bar 100% transparente + overlay `UIVisualEffectView`
85
- /// con `UIGlassEffect` (iOS 26+) o `systemThinMaterial` fallback.
86
132
  private func applyAppearance(_ tabBar: UITabBar, style: LiquidGlassTabBarStyle) {
87
133
  let appearance = UITabBarAppearance()
88
134
  switch style {
89
- 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.
90
139
  appearance.configureWithDefaultBackground()
91
- removeGlassOverlay()
92
140
  case .ultraThin:
93
141
  appearance.configureWithDefaultBackground()
94
142
  appearance.backgroundEffect = UIBlurEffect(style: .systemUltraThinMaterial)
95
143
  appearance.backgroundColor = UIColor.clear
96
- removeGlassOverlay()
97
144
  case .transparent:
98
145
  appearance.configureWithTransparentBackground()
99
146
  appearance.backgroundEffect = nil
100
147
  appearance.backgroundColor = UIColor.clear
101
148
  appearance.shadowColor = UIColor.clear
102
- removeGlassOverlay()
103
- case .liquidGlass:
104
- // Tab bar 100% transparente — el material lo aporta el overlay
105
- // `UIVisualEffectView` con `UIGlassEffect` montado debajo del bar.
106
- // Es el patrón canon de iOS 26 para aplicar Liquid Glass real a
107
- // controles que no exponen API directa para asignarlo (UITabBar
108
- // solo acepta `UIBlurEffect` en `backgroundEffect`).
109
- appearance.configureWithTransparentBackground()
110
- appearance.backgroundEffect = nil
111
- appearance.backgroundColor = UIColor.clear
112
- appearance.shadowColor = UIColor.clear
113
- installGlassOverlay(behind: tabBar)
114
149
  }
115
150
  tabBar.standardAppearance = appearance
116
151
  if #available(iOS 15.0, *) {
@@ -118,53 +153,7 @@ final class LiquidGlassTabBarOverlay: NSObject {
118
153
  }
119
154
  }
120
155
 
121
- /// Monta un `UIVisualEffectView` con `UIGlassEffect` (iOS 26+) o
122
- /// `systemThinMaterial` como fallback, justo DEBAJO del `tabBar` en el
123
- /// z-order del window. `tabBar` está en modo transparente, así que todo
124
- /// el material visible viene del overlay.
125
- ///
126
- /// El overlay queda con `isUserInteractionEnabled = false` para no
127
- /// interferir con los taps que el `tabBar` consume por encima.
128
- private func installGlassOverlay(behind tabBar: UITabBar) {
129
- guard let parent = tabBar.superview else { return }
130
- // Idempotente: si ya existe overlay no recrearlo (evita flicker en
131
- // re-configs por badge updates u otros cambios sin style change).
132
- if glassOverlayView != nil { return }
133
-
134
- let effect: UIVisualEffect
135
- #if compiler(>=6.1)
136
- if #available(iOS 26.0, *) {
137
- effect = UIGlassEffect()
138
- } else {
139
- effect = UIBlurEffect(style: .systemThinMaterial)
140
- }
141
- #else
142
- effect = UIBlurEffect(style: .systemThinMaterial)
143
- #endif
144
-
145
- let overlay = UIVisualEffectView(effect: effect)
146
- overlay.translatesAutoresizingMaskIntoConstraints = false
147
- overlay.isUserInteractionEnabled = false
148
- parent.insertSubview(overlay, belowSubview: tabBar)
149
-
150
- NSLayoutConstraint.activate([
151
- overlay.leadingAnchor.constraint(equalTo: tabBar.leadingAnchor),
152
- overlay.trailingAnchor.constraint(equalTo: tabBar.trailingAnchor),
153
- overlay.topAnchor.constraint(equalTo: tabBar.topAnchor),
154
- overlay.bottomAnchor.constraint(equalTo: tabBar.bottomAnchor),
155
- ])
156
-
157
- self.glassOverlayView = overlay
158
- }
159
-
160
- private func removeGlassOverlay() {
161
- glassOverlayView?.removeFromSuperview()
162
- glassOverlayView = nil
163
- }
164
-
165
156
  func configure(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?, style: LiquidGlassTabBarStyle) {
166
- guard let tabBar else { return }
167
-
168
157
  // Preserve selection across re-configs (badge changes, etc.)
169
158
  let previousSelectedId: String? = tabBar.selectedItem.flatMap { current in
170
159
  self.items.indices.contains(current.tag) ? self.items[current.tag].id : nil
@@ -189,8 +178,6 @@ final class LiquidGlassTabBarOverlay: NSObject {
189
178
 
190
179
  tabBar.setItems(uiItems, animated: false)
191
180
 
192
- // Si el caller pidió explícitamente "no selection" (selectedIndex < 0),
193
- // respetar eso y NO restaurar la selección previa — deja selectedItem = nil.
194
181
  if selectedIndex < 0 {
195
182
  tabBar.selectedItem = nil
196
183
  } else if let previousSelectedId, let restoreIdx = items.firstIndex(where: { $0.id == previousSelectedId }) {
@@ -206,7 +193,7 @@ final class LiquidGlassTabBarOverlay: NSObject {
206
193
  }
207
194
 
208
195
  func updateBadge(id: String, badge: String?) {
209
- guard let tabBar, let tabBarItems = tabBar.items,
196
+ guard let tabBarItems = tabBar.items,
210
197
  let idx = items.firstIndex(where: { $0.id == id }),
211
198
  idx < tabBarItems.count else { return }
212
199
  let value = (badge?.isEmpty ?? true) ? nil : badge
@@ -214,20 +201,10 @@ final class LiquidGlassTabBarOverlay: NSObject {
214
201
  }
215
202
 
216
203
  func show() {
217
- guard let tabBar = tabBar else { return }
218
- // Si ya está visible, no hacer nada (evita flicker en re-show consecutivos)
219
204
  if !tabBar.isHidden && tabBar.alpha == 1.0 { emitLayout(); return }
220
- // Reset estado pre-animación
221
205
  tabBar.isHidden = false
222
206
  tabBar.alpha = 0
223
207
  tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
224
- // Sincronizar el overlay glass (si está montado) con la misma curva,
225
- // sino el material queda visible mientras el bar fade-out/in.
226
- let overlay = glassOverlayView
227
- overlay?.isHidden = false
228
- overlay?.alpha = 0
229
- overlay?.transform = CGAffineTransform(translationX: 0, y: 20)
230
- // Fade-in + slide-up (curva native iOS para apariciones)
231
208
  UIView.animate(
232
209
  withDuration: 0.28,
233
210
  delay: 0,
@@ -235,10 +212,8 @@ final class LiquidGlassTabBarOverlay: NSObject {
235
212
  initialSpringVelocity: 0,
236
213
  options: [.curveEaseOut, .allowUserInteraction],
237
214
  animations: {
238
- tabBar.alpha = 1
239
- tabBar.transform = .identity
240
- overlay?.alpha = 1
241
- overlay?.transform = .identity
215
+ self.tabBar.alpha = 1
216
+ self.tabBar.transform = .identity
242
217
  },
243
218
  completion: nil
244
219
  )
@@ -246,32 +221,24 @@ final class LiquidGlassTabBarOverlay: NSObject {
246
221
  }
247
222
 
248
223
  func hide() {
249
- guard let tabBar = tabBar else { return }
250
224
  if tabBar.isHidden { return }
251
- // Fade-out + slide-down (más rápido que el show — el exit es snappy)
252
- let overlay = glassOverlayView
253
225
  UIView.animate(
254
226
  withDuration: 0.18,
255
227
  delay: 0,
256
228
  options: [.curveEaseIn, .allowUserInteraction],
257
229
  animations: {
258
- tabBar.alpha = 0
259
- tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
260
- overlay?.alpha = 0
261
- overlay?.transform = CGAffineTransform(translationX: 0, y: 20)
230
+ self.tabBar.alpha = 0
231
+ self.tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
262
232
  },
263
233
  completion: { _ in
264
- tabBar.isHidden = true
265
- tabBar.transform = .identity
266
- overlay?.isHidden = true
267
- overlay?.transform = .identity
234
+ self.tabBar.isHidden = true
235
+ self.tabBar.transform = .identity
268
236
  }
269
237
  )
270
238
  }
271
239
 
272
240
  func setSelectedIndex(_ index: Int) {
273
- guard let tabBar, let items = tabBar.items else { return }
274
- // index < 0 → deseleccionar todos los tabs (selectedItem = nil)
241
+ guard let items = tabBar.items else { return }
275
242
  if index < 0 {
276
243
  tabBar.selectedItem = nil
277
244
  return
@@ -286,7 +253,6 @@ final class LiquidGlassTabBarOverlay: NSObject {
286
253
  }
287
254
 
288
255
  func currentLayout() -> (height: Double, bottomSafeArea: Double) {
289
- guard let tabBar else { return (0, 0) }
290
256
  tabBar.layoutIfNeeded()
291
257
  let bottomInset = tabBar.safeAreaInsets.bottom
292
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.1",
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",