@ajuarezso/capacitor-liquid-glass 0.3.4 → 0.3.6

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.
@@ -152,7 +152,12 @@ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
152
152
  }
153
153
 
154
154
  private func presentTabBar(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?, styleRaw: String) {
155
- guard let window = UIApplication.shared.capacitorWindow else { return }
155
+ // CRÍTICO: usar `bridge?.viewController` (el VC que contiene el
156
+ // WKWebView de Capacitor) en lugar del `rootViewController` del
157
+ // window. iOS 26 aplica Liquid Glass automáticamente al UITabBar
158
+ // solo cuando está en la jerarquía del VC del webview. El rootVC
159
+ // del window puede ser un container distinto y romper el adopt.
160
+ guard let hostVC = bridge?.viewController else { return }
156
161
 
157
162
  if tabBarOverlay == nil {
158
163
  let overlay = LiquidGlassTabBarOverlay()
@@ -169,7 +174,7 @@ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
169
174
  }
170
175
 
171
176
  let style = LiquidGlassTabBarStyle(rawValue: styleRaw) ?? .default
172
- tabBarOverlay?.attach(to: window)
177
+ tabBarOverlay?.attach(to: hostVC)
173
178
  tabBarOverlay?.configure(items: items, selectedIndex: selectedIndex, tintHex: tintHex, style: style)
174
179
  tabBarOverlay?.show()
175
180
  }
@@ -42,14 +42,18 @@ enum LiquidGlassTabBarStyle: String {
42
42
  /// app se compila contra el SDK iOS 26.
43
43
  ///
44
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.
45
+ /// CHILD del view controller que contiene el WKWebView de Capacitor
46
+ /// (`bridge?.viewController`), y el `UITabBar` vive como subview de
47
+ /// `self.view`. Esto crea el view controller hierarchy completo que iOS 26
48
+ /// necesita para aplicar el material Liquid Glass automáticamente al
49
+ /// UITabBar.
48
50
  ///
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.
51
+ /// Por qué NO usar `window.rootViewController`: en Capacitor el rootVC del
52
+ /// window puede ser un container distinto al VC del webview. iOS 26 solo
53
+ /// aplica Liquid Glass auto cuando el UITabBar vive en la jerarquía del VC
54
+ /// que tiene el webview activo. Agregarlo al rootVC del window puede
55
+ /// romper el auto-adopt y dejarlo con material translúcido genérico opaco
56
+ /// (no el real de Music / App Store).
53
57
  ///
54
58
  /// Patrón inspirado en `stay-liquid` (alistairheath/stay-liquid GitHub).
55
59
  final class LiquidGlassTabBarOverlay: UIViewController {
@@ -66,7 +70,7 @@ final class LiquidGlassTabBarOverlay: UIViewController {
66
70
  override func viewDidLoad() {
67
71
  super.viewDidLoad()
68
72
  // Background transparente. El `self.view` está sizado SOLO al rect
69
- // del tab bar (leading/trailing/bottom al rootVC, sin top — la
73
+ // del tab bar (leading/trailing/bottom al hostVC, sin top — la
70
74
  // altura la determina el intrinsic content size del UITabBar dentro).
71
75
  // Por eso NO necesitamos `hitTest` override: el view en sí solo
72
76
  // existe sobre el área de la pill, los taps fuera no llegan.
@@ -75,9 +79,13 @@ final class LiquidGlassTabBarOverlay: UIViewController {
75
79
  tabBar.translatesAutoresizingMaskIntoConstraints = false
76
80
  tabBar.delegate = self
77
81
 
78
- // Default appearance: triggers Liquid Glass automatically on iOS 26+.
79
- // Sobreescrito en `configure(...)` si el caller pidió otro estilo.
80
- applyAppearance(tabBar, style: .default)
82
+ // CRÍTICO: NO setear appearance acá. iOS 26 adopta Liquid Glass
83
+ // automáticamente SOLO si el UITabBar mantiene su appearance default
84
+ // intacto. Tocar `standardAppearance` / `scrollEdgeAppearance` en
85
+ // CUALQUIER forma — incluso con `configureWithDefaultBackground()` —
86
+ // anula el adopt y deja un material translúcido genérico opaco.
87
+ // Solo los overrides intencionales (.ultraThin, .transparent) tocan
88
+ // el appearance, vía `applyAppearance(...)` desde `configure(...)`.
81
89
 
82
90
  view.addSubview(tabBar)
83
91
 
@@ -91,12 +99,13 @@ final class LiquidGlassTabBarOverlay: UIViewController {
91
99
  ])
92
100
  }
93
101
 
94
- /// Adjunta este view controller como child del rootViewController del
95
- /// window. Idempotente: si ya está adjuntado al mismo root, no-op.
96
- func attach(to window: UIWindow) {
97
- guard let rootVC = window.rootViewController else { return }
98
- if self.parent === rootVC { return }
99
- // Si está adjuntado a otro VC (cambio de root entre sesiones),
102
+ /// Adjunta este view controller como child del view controller que
103
+ /// contiene el WKWebView de Capacitor (`bridge?.viewController`).
104
+ /// Idempotente: si ya está adjuntado al mismo host, no-op.
105
+ func attach(to hostVC: UIViewController?) {
106
+ guard let hostVC else { return }
107
+ if self.parent === hostVC { return }
108
+ // Si está adjuntado a otro VC (cambio de host entre sesiones),
100
109
  // detachar primero antes de re-adjuntar.
101
110
  if self.parent != nil {
102
111
  self.willMove(toParent: nil)
@@ -104,37 +113,45 @@ final class LiquidGlassTabBarOverlay: UIViewController {
104
113
  self.removeFromParent()
105
114
  }
106
115
 
107
- rootVC.addChild(self)
116
+ hostVC.addChild(self)
117
+ hostVC.view.addSubview(self.view)
108
118
  self.view.translatesAutoresizingMaskIntoConstraints = false
109
- rootVC.view.addSubview(self.view)
119
+ // CRÍTICO: `didMove(toParent:)` ANTES de activar constraints. iOS 26
120
+ // ejecuta el primer layout pass al didMove; tener el parenting
121
+ // completo antes de que el sistema mida el UITabBar es lo que el
122
+ // heurístico de Liquid Glass auto-adopt espera (patrón stay-liquid).
123
+ self.didMove(toParent: hostVC)
110
124
  // SIN top constraint — la altura del view se deriva del intrinsic
111
125
  // content size del UITabBar dentro (~50pt + safe-area-bottom).
112
- // Replica del patrón stay-liquid (validado en producción) que
113
- // permite a iOS 26 detectar este UITabBar como "floating tab bar"
114
- // y aplicarle Liquid Glass automáticamente.
115
126
  NSLayoutConstraint.activate([
116
- self.view.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
117
- self.view.trailingAnchor.constraint(equalTo: rootVC.view.trailingAnchor),
118
- self.view.bottomAnchor.constraint(equalTo: rootVC.view.bottomAnchor),
127
+ self.view.leadingAnchor.constraint(equalTo: hostVC.view.leadingAnchor),
128
+ self.view.trailingAnchor.constraint(equalTo: hostVC.view.trailingAnchor),
129
+ self.view.bottomAnchor.constraint(equalTo: hostVC.view.bottomAnchor),
119
130
  ])
120
- self.didMove(toParent: rootVC)
121
- self.hostVC = rootVC
131
+ self.hostVC = hostVC
122
132
  emitLayout()
123
133
  }
124
134
 
125
135
  /// Aplica el appearance correspondiente al estilo solicitado.
126
- /// - `default` / `liquidGlass`: Liquid Glass nativo iOS 26 (SDK 26 + runtime
127
- /// 26). En iOS < 26 cae al blur translúcido legacy.
136
+ /// - `default` / `liquidGlass`: **NO-OP** el UITabBar mantiene su
137
+ /// appearance default sin tocar para que iOS 26 aplique Liquid Glass
138
+ /// automáticamente. Cualquier touch al appearance (incluso
139
+ /// `configureWithDefaultBackground()`) rompe el auto-adopt.
128
140
  /// - `ultraThin`: blur mínimo `systemUltraThinMaterial` — más ver-through.
141
+ /// Override intencional que CANCELA Liquid Glass.
129
142
  /// - `transparent`: sin background ni blur — content behind se ve completo.
143
+ /// Override intencional que CANCELA Liquid Glass.
130
144
  private func applyAppearance(_ tabBar: UITabBar, style: LiquidGlassTabBarStyle) {
145
+ // Early-return para los estilos que confían en el adopt automático
146
+ // del sistema. Tocar appearance acá anula Liquid Glass.
147
+ if style == .default || style == .liquidGlass { return }
148
+
131
149
  let appearance = UITabBarAppearance()
132
150
  switch style {
133
151
  case .default, .liquidGlass:
134
- // En iOS 26+ con SDK 26, esto adopta Liquid Glass automáticamente.
135
- // NO hacer override de `backgroundEffect` — eso anula el adopt y
136
- // produce un material opaco genérico.
137
- appearance.configureWithDefaultBackground()
152
+ // Unreachable early-return arriba. Mantenido por exhaustividad
153
+ // del switch sobre el enum.
154
+ return
138
155
  case .ultraThin:
139
156
  appearance.configureWithDefaultBackground()
140
157
  appearance.backgroundEffect = UIBlurEffect(style: .systemUltraThinMaterial)
@@ -174,7 +191,12 @@ final class LiquidGlassTabBarOverlay: UIViewController {
174
191
  return tab
175
192
  }
176
193
 
177
- tabBar.setItems(uiItems, animated: false)
194
+ // CRÍTICO: setter directo `tabBar.items = ...` en lugar de
195
+ // `tabBar.setItems(_, animated: false)`. En iOS 26 el método
196
+ // `setItems(animated:)` toma un path interno distinto que puede
197
+ // invalidar el adopt automático de Liquid Glass. El setter directo
198
+ // es el que stay-liquid usa (línea 98 de su TabsBarOverlay).
199
+ tabBar.items = uiItems
178
200
 
179
201
  if selectedIndex < 0 {
180
202
  tabBar.selectedItem = nil
@@ -198,41 +220,23 @@ final class LiquidGlassTabBarOverlay: UIViewController {
198
220
  tabBarItems[idx].badgeValue = value
199
221
  }
200
222
 
223
+ /// CRÍTICO: NO tocar `tabBar.alpha`, `tabBar.transform` ni `tabBar.isHidden`.
224
+ /// iOS 26 inspecciona el UITabBar en el primer layout pass para decidir si
225
+ /// adopta Liquid Glass. Si en ese momento el bar está con `alpha=0` o un
226
+ /// `transform` distinto de `.identity`, el sistema descarta el adopt y NO
227
+ /// re-evalúa aunque después vuelva a valores normales.
228
+ ///
229
+ /// Para animar la aparición/desaparición sin perder el adopt, se manipula
230
+ /// `self.view.isHidden` (el contenedor del VC), no el UITabBar mismo.
231
+ /// Patrón validado en stay-liquid (su `update()` simplemente hace
232
+ /// `view.isHidden = !visible`).
201
233
  func show() {
202
- if !tabBar.isHidden && tabBar.alpha == 1.0 { emitLayout(); return }
203
- tabBar.isHidden = false
204
- tabBar.alpha = 0
205
- tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
206
- UIView.animate(
207
- withDuration: 0.28,
208
- delay: 0,
209
- usingSpringWithDamping: 0.85,
210
- initialSpringVelocity: 0,
211
- options: [.curveEaseOut, .allowUserInteraction],
212
- animations: {
213
- self.tabBar.alpha = 1
214
- self.tabBar.transform = .identity
215
- },
216
- completion: nil
217
- )
234
+ view.isHidden = false
218
235
  emitLayout()
219
236
  }
220
237
 
221
238
  func hide() {
222
- if tabBar.isHidden { return }
223
- UIView.animate(
224
- withDuration: 0.18,
225
- delay: 0,
226
- options: [.curveEaseIn, .allowUserInteraction],
227
- animations: {
228
- self.tabBar.alpha = 0
229
- self.tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
230
- },
231
- completion: { _ in
232
- self.tabBar.isHidden = true
233
- self.tabBar.transform = .identity
234
- }
235
- )
239
+ view.isHidden = true
236
240
  }
237
241
 
238
242
  func setSelectedIndex(_ index: Int) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ajuarezso/capacitor-liquid-glass",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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",