@ajuarezso/capacitor-liquid-glass 0.3.1 → 0.3.3
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,142 @@ 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
|
-
|
|
52
|
-
///
|
|
53
|
-
///
|
|
54
|
-
|
|
64
|
+
private weak var hostVC: UIViewController?
|
|
65
|
+
|
|
66
|
+
/// Reemplaza `self.view` por una `PassThroughView` custom. El override
|
|
67
|
+
/// de `hitTest` vive en la `UIView` (no en el `UIViewController`), así
|
|
68
|
+
/// que necesitamos una subclase dedicada en lugar de override el VC.
|
|
69
|
+
override func loadView() {
|
|
70
|
+
let v = PassThroughView()
|
|
71
|
+
v.allowedHitView = tabBar
|
|
72
|
+
self.view = v
|
|
73
|
+
}
|
|
55
74
|
|
|
56
|
-
func
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
override func viewDidLoad() {
|
|
76
|
+
super.viewDidLoad()
|
|
77
|
+
// Background transparente — solo el `UITabBar` (la pill flotante)
|
|
78
|
+
// ocupa espacio visible. El resto del área es transparente y la
|
|
79
|
+
// `PassThroughView` deja pasar los toques al webview por debajo.
|
|
80
|
+
view.backgroundColor = .clear
|
|
59
81
|
|
|
60
|
-
let tabBar = UITabBar()
|
|
61
82
|
tabBar.translatesAutoresizingMaskIntoConstraints = false
|
|
62
83
|
tabBar.delegate = self
|
|
63
84
|
|
|
64
85
|
// Default appearance: triggers Liquid Glass automatically on iOS 26+.
|
|
65
|
-
//
|
|
86
|
+
// Sobreescrito en `configure(...)` si el caller pidió otro estilo.
|
|
66
87
|
applyAppearance(tabBar, style: .default)
|
|
67
88
|
|
|
68
|
-
|
|
89
|
+
view.addSubview(tabBar)
|
|
69
90
|
|
|
70
91
|
NSLayoutConstraint.activate([
|
|
71
|
-
tabBar.leadingAnchor.constraint(equalTo:
|
|
72
|
-
tabBar.trailingAnchor.constraint(equalTo:
|
|
73
|
-
tabBar.bottomAnchor.constraint(equalTo:
|
|
92
|
+
tabBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
93
|
+
tabBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
94
|
+
tabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
74
95
|
])
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// View que cubre todo el rootVC pero solo absorbe taps que aterrizan
|
|
99
|
+
/// sobre `allowedHitView` (el `UITabBar` floating). El resto pasa al
|
|
100
|
+
/// webview de Capacitor por debajo. Sin esto, este overlay bloquearía
|
|
101
|
+
/// TODO el contenido de la app.
|
|
102
|
+
private final class PassThroughView: UIView {
|
|
103
|
+
weak var allowedHitView: UIView?
|
|
104
|
+
|
|
105
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
106
|
+
let hit = super.hitTest(point, with: event)
|
|
107
|
+
guard let hit, let allowed = allowedHitView else { return nil }
|
|
108
|
+
if hit === allowed || hit.isDescendant(of: allowed) { return hit }
|
|
109
|
+
return nil
|
|
110
|
+
}
|
|
111
|
+
}
|
|
75
112
|
|
|
76
|
-
|
|
113
|
+
/// Adjunta este view controller como child del rootViewController del
|
|
114
|
+
/// window. Idempotente: si ya está adjuntado al mismo root, no-op.
|
|
115
|
+
func attach(to window: UIWindow) {
|
|
116
|
+
guard let rootVC = window.rootViewController else { return }
|
|
117
|
+
if self.parent === rootVC { return }
|
|
118
|
+
// Si está adjuntado a otro VC (cambio de root entre sesiones),
|
|
119
|
+
// detachar primero antes de re-adjuntar.
|
|
120
|
+
if self.parent != nil {
|
|
121
|
+
self.willMove(toParent: nil)
|
|
122
|
+
self.view.removeFromSuperview()
|
|
123
|
+
self.removeFromParent()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
rootVC.addChild(self)
|
|
127
|
+
self.view.translatesAutoresizingMaskIntoConstraints = false
|
|
128
|
+
rootVC.view.addSubview(self.view)
|
|
129
|
+
NSLayoutConstraint.activate([
|
|
130
|
+
self.view.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
|
|
131
|
+
self.view.trailingAnchor.constraint(equalTo: rootVC.view.trailingAnchor),
|
|
132
|
+
self.view.topAnchor.constraint(equalTo: rootVC.view.topAnchor),
|
|
133
|
+
self.view.bottomAnchor.constraint(equalTo: rootVC.view.bottomAnchor),
|
|
134
|
+
])
|
|
135
|
+
self.didMove(toParent: rootVC)
|
|
136
|
+
self.hostVC = rootVC
|
|
77
137
|
emitLayout()
|
|
78
138
|
}
|
|
79
139
|
|
|
80
140
|
/// Aplica el appearance correspondiente al estilo solicitado.
|
|
81
|
-
/// - `default`: Liquid Glass
|
|
141
|
+
/// - `default` / `liquidGlass`: Liquid Glass nativo iOS 26 (SDK 26 + runtime
|
|
142
|
+
/// 26). En iOS < 26 cae al blur translúcido legacy.
|
|
82
143
|
/// - `ultraThin`: blur mínimo `systemUltraThinMaterial` — más ver-through.
|
|
83
144
|
/// - `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
145
|
private func applyAppearance(_ tabBar: UITabBar, style: LiquidGlassTabBarStyle) {
|
|
87
146
|
let appearance = UITabBarAppearance()
|
|
88
147
|
switch style {
|
|
89
|
-
case .default:
|
|
148
|
+
case .default, .liquidGlass:
|
|
149
|
+
// En iOS 26+ con SDK 26, esto adopta Liquid Glass automáticamente.
|
|
150
|
+
// NO hacer override de `backgroundEffect` — eso anula el adopt y
|
|
151
|
+
// produce un material opaco genérico.
|
|
90
152
|
appearance.configureWithDefaultBackground()
|
|
91
|
-
removeGlassOverlay()
|
|
92
153
|
case .ultraThin:
|
|
93
154
|
appearance.configureWithDefaultBackground()
|
|
94
155
|
appearance.backgroundEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
|
95
156
|
appearance.backgroundColor = UIColor.clear
|
|
96
|
-
removeGlassOverlay()
|
|
97
157
|
case .transparent:
|
|
98
158
|
appearance.configureWithTransparentBackground()
|
|
99
159
|
appearance.backgroundEffect = nil
|
|
100
160
|
appearance.backgroundColor = UIColor.clear
|
|
101
161
|
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
162
|
}
|
|
115
163
|
tabBar.standardAppearance = appearance
|
|
116
164
|
if #available(iOS 15.0, *) {
|
|
@@ -118,53 +166,7 @@ final class LiquidGlassTabBarOverlay: NSObject {
|
|
|
118
166
|
}
|
|
119
167
|
}
|
|
120
168
|
|
|
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
169
|
func configure(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?, style: LiquidGlassTabBarStyle) {
|
|
166
|
-
guard let tabBar else { return }
|
|
167
|
-
|
|
168
170
|
// Preserve selection across re-configs (badge changes, etc.)
|
|
169
171
|
let previousSelectedId: String? = tabBar.selectedItem.flatMap { current in
|
|
170
172
|
self.items.indices.contains(current.tag) ? self.items[current.tag].id : nil
|
|
@@ -189,8 +191,6 @@ final class LiquidGlassTabBarOverlay: NSObject {
|
|
|
189
191
|
|
|
190
192
|
tabBar.setItems(uiItems, animated: false)
|
|
191
193
|
|
|
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
194
|
if selectedIndex < 0 {
|
|
195
195
|
tabBar.selectedItem = nil
|
|
196
196
|
} else if let previousSelectedId, let restoreIdx = items.firstIndex(where: { $0.id == previousSelectedId }) {
|
|
@@ -206,7 +206,7 @@ final class LiquidGlassTabBarOverlay: NSObject {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
func updateBadge(id: String, badge: String?) {
|
|
209
|
-
guard let
|
|
209
|
+
guard let tabBarItems = tabBar.items,
|
|
210
210
|
let idx = items.firstIndex(where: { $0.id == id }),
|
|
211
211
|
idx < tabBarItems.count else { return }
|
|
212
212
|
let value = (badge?.isEmpty ?? true) ? nil : badge
|
|
@@ -214,20 +214,10 @@ final class LiquidGlassTabBarOverlay: NSObject {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
func show() {
|
|
217
|
-
guard let tabBar = tabBar else { return }
|
|
218
|
-
// Si ya está visible, no hacer nada (evita flicker en re-show consecutivos)
|
|
219
217
|
if !tabBar.isHidden && tabBar.alpha == 1.0 { emitLayout(); return }
|
|
220
|
-
// Reset estado pre-animación
|
|
221
218
|
tabBar.isHidden = false
|
|
222
219
|
tabBar.alpha = 0
|
|
223
220
|
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
221
|
UIView.animate(
|
|
232
222
|
withDuration: 0.28,
|
|
233
223
|
delay: 0,
|
|
@@ -235,10 +225,8 @@ final class LiquidGlassTabBarOverlay: NSObject {
|
|
|
235
225
|
initialSpringVelocity: 0,
|
|
236
226
|
options: [.curveEaseOut, .allowUserInteraction],
|
|
237
227
|
animations: {
|
|
238
|
-
tabBar.alpha = 1
|
|
239
|
-
tabBar.transform = .identity
|
|
240
|
-
overlay?.alpha = 1
|
|
241
|
-
overlay?.transform = .identity
|
|
228
|
+
self.tabBar.alpha = 1
|
|
229
|
+
self.tabBar.transform = .identity
|
|
242
230
|
},
|
|
243
231
|
completion: nil
|
|
244
232
|
)
|
|
@@ -246,32 +234,24 @@ final class LiquidGlassTabBarOverlay: NSObject {
|
|
|
246
234
|
}
|
|
247
235
|
|
|
248
236
|
func hide() {
|
|
249
|
-
guard let tabBar = tabBar else { return }
|
|
250
237
|
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
238
|
UIView.animate(
|
|
254
239
|
withDuration: 0.18,
|
|
255
240
|
delay: 0,
|
|
256
241
|
options: [.curveEaseIn, .allowUserInteraction],
|
|
257
242
|
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)
|
|
243
|
+
self.tabBar.alpha = 0
|
|
244
|
+
self.tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
|
|
262
245
|
},
|
|
263
246
|
completion: { _ in
|
|
264
|
-
tabBar.isHidden = true
|
|
265
|
-
tabBar.transform = .identity
|
|
266
|
-
overlay?.isHidden = true
|
|
267
|
-
overlay?.transform = .identity
|
|
247
|
+
self.tabBar.isHidden = true
|
|
248
|
+
self.tabBar.transform = .identity
|
|
268
249
|
}
|
|
269
250
|
)
|
|
270
251
|
}
|
|
271
252
|
|
|
272
253
|
func setSelectedIndex(_ index: Int) {
|
|
273
|
-
guard let
|
|
274
|
-
// index < 0 → deseleccionar todos los tabs (selectedItem = nil)
|
|
254
|
+
guard let items = tabBar.items else { return }
|
|
275
255
|
if index < 0 {
|
|
276
256
|
tabBar.selectedItem = nil
|
|
277
257
|
return
|
|
@@ -286,7 +266,6 @@ final class LiquidGlassTabBarOverlay: NSObject {
|
|
|
286
266
|
}
|
|
287
267
|
|
|
288
268
|
func currentLayout() -> (height: Double, bottomSafeArea: Double) {
|
|
289
|
-
guard let tabBar else { return (0, 0) }
|
|
290
269
|
tabBar.layoutIfNeeded()
|
|
291
270
|
let bottomInset = tabBar.safeAreaInsets.bottom
|
|
292
271
|
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.3",
|
|
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",
|