@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 —
|
|
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`) —
|
|
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
|
|
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
|
-
///
|
|
33
|
-
///
|
|
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
|
-
///
|
|
38
|
-
///
|
|
39
|
-
///
|
|
40
|
-
|
|
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
|
|
48
|
-
private var tabBar: UITabBar?
|
|
62
|
+
private let tabBar = UITabBar()
|
|
49
63
|
private var items: [LiquidGlassTabItem] = []
|
|
50
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
//
|
|
77
|
+
// Sobreescrito en `configure(...)` si el caller pidió otro estilo.
|
|
66
78
|
applyAppearance(tabBar, style: .default)
|
|
67
79
|
|
|
68
|
-
|
|
80
|
+
view.addSubview(tabBar)
|
|
69
81
|
|
|
70
82
|
NSLayoutConstraint.activate([
|
|
71
|
-
tabBar.leadingAnchor.constraint(equalTo:
|
|
72
|
-
tabBar.trailingAnchor.constraint(equalTo:
|
|
73
|
-
tabBar.bottomAnchor.constraint(equalTo:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|