@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 —
|
|
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] = []
|
|
64
|
+
private weak var hostVC: UIViewController?
|
|
50
65
|
|
|
51
|
-
func
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
//
|
|
77
|
+
// Sobreescrito en `configure(...)` si el caller pidió otro estilo.
|
|
61
78
|
applyAppearance(tabBar, style: .default)
|
|
62
79
|
|
|
63
|
-
|
|
80
|
+
view.addSubview(tabBar)
|
|
64
81
|
|
|
65
82
|
NSLayoutConstraint.activate([
|
|
66
|
-
tabBar.leadingAnchor.constraint(equalTo:
|
|
67
|
-
tabBar.trailingAnchor.constraint(equalTo:
|
|
68
|
-
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),
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|