@k-workspace/react-native-liquidglass-view 0.1.0
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.
- package/README.md +140 -0
- package/ios/BNGlassContainerView.swift +89 -0
- package/ios/BNGlassView.swift +319 -0
- package/ios/LiquidGlassContainerManager.m +21 -0
- package/ios/LiquidGlassViewManager.m +36 -0
- package/lib/NativeLiquidGlass.d.ts +63 -0
- package/lib/NativeLiquidGlass.d.ts.map +1 -0
- package/lib/NativeLiquidGlass.js +9 -0
- package/lib/NativeLiquidGlass.js.map +1 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +36 -0
- package/lib/index.js.map +1 -0
- package/package.json +53 -0
- package/react-native-liquidGlass-view.podspec +21 -0
- package/react-native.config.js +10 -0
- package/src/NativeLiquidGlass.ts +77 -0
- package/src/index.tsx +145 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# @k-workspace/react-native-liquidglass-view
|
|
2
|
+
|
|
3
|
+
iOS 26 Liquid Glass effect for React Native. Wraps Apple's native `UIGlassEffect` with configurable styles, shadows, press animations, entrance transitions, and glass container merging.
|
|
4
|
+
|
|
5
|
+
> **iOS 26+ only.** On older iOS versions and Android, a translucent fallback view is rendered automatically.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @k-workspace/react-native-liquidglass-view
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then install the native iOS dependency:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd ios && pod install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
| Requirement | Version |
|
|
22
|
+
| -------------- | ----------------------- |
|
|
23
|
+
| React Native | >= 0.73 |
|
|
24
|
+
| iOS Deployment | >= 15.1 |
|
|
25
|
+
| Xcode | 26+ (for UIGlassEffect) |
|
|
26
|
+
| Swift | 5.0+ |
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### GlassCard
|
|
31
|
+
|
|
32
|
+
An opinionated card component with sensible defaults — the easiest way to add glass surfaces.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { GlassCard } from "@k-workspace/react-native-liquidglass-view";
|
|
36
|
+
|
|
37
|
+
<GlassCard cornerRadius={20} effect="clear">
|
|
38
|
+
<Text>Hello, Glass</Text>
|
|
39
|
+
</GlassCard>;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### GlassView
|
|
43
|
+
|
|
44
|
+
A lower-level component exposing every native prop for full control.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { GlassView } from "@k-workspace/react-native-liquidglass-view";
|
|
48
|
+
|
|
49
|
+
<GlassView
|
|
50
|
+
style={{ width: 300, height: 200 }}
|
|
51
|
+
effect="regular"
|
|
52
|
+
cornerRadius={24}
|
|
53
|
+
tintColor="rgba(100, 120, 255, 0.15)"
|
|
54
|
+
colorScheme="dark"
|
|
55
|
+
interactive={true}
|
|
56
|
+
shadowIntensity={0.2}
|
|
57
|
+
enablePressAnimation
|
|
58
|
+
enableEntrance
|
|
59
|
+
>
|
|
60
|
+
<Text>Full control</Text>
|
|
61
|
+
</GlassView>;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### GlassContainer
|
|
65
|
+
|
|
66
|
+
Groups child glass views so their edges merge together (uses `UIGlassContainerEffect`).
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import {
|
|
70
|
+
GlassContainer,
|
|
71
|
+
GlassView,
|
|
72
|
+
} from "@k-workspace/react-native-liquidglass-view";
|
|
73
|
+
|
|
74
|
+
<GlassContainer spacing={4}>
|
|
75
|
+
<GlassView style={{ flex: 1 }}>
|
|
76
|
+
<Text>Left</Text>
|
|
77
|
+
</GlassView>
|
|
78
|
+
<GlassView style={{ flex: 1 }}>
|
|
79
|
+
<Text>Right</Text>
|
|
80
|
+
</GlassView>
|
|
81
|
+
</GlassContainer>;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Props
|
|
85
|
+
|
|
86
|
+
### LiquidGlassProps (GlassCard & GlassView)
|
|
87
|
+
|
|
88
|
+
| Prop | Type | Default | Description |
|
|
89
|
+
| ---------------------- | ---------------------- | ----------- | -------------------------------------------------- |
|
|
90
|
+
| `effect` | `'regular' \| 'clear'` | `'clear'` | Visual weight of the glass material. |
|
|
91
|
+
| `cornerRadius` | `number` | `16` | Corner radius with continuous (Apple-style) curve. |
|
|
92
|
+
| `tintColor` | `string` | `undefined` | Semi-transparent color blended over the glass. |
|
|
93
|
+
| `colorScheme` | `'light' \| 'dark'` | System | Override the system color scheme for this view. |
|
|
94
|
+
| `interactive` | `boolean` | `true` | Whether the glass responds to touch. |
|
|
95
|
+
| `shadowIntensity` | `number` | `0.15` | Elevation shadow opacity (0 – 1). |
|
|
96
|
+
| `enablePressAnimation` | `boolean` | `false` | Spring scale animation + haptic on press. |
|
|
97
|
+
| `enableEntrance` | `boolean` | `false` | Fade + translate-up entrance animation. |
|
|
98
|
+
|
|
99
|
+
### LiquidGlassContainerProps (GlassContainer)
|
|
100
|
+
|
|
101
|
+
| Prop | Type | Default | Description |
|
|
102
|
+
| --------- | -------- | ------- | ------------------------------------------------------ |
|
|
103
|
+
| `spacing` | `number` | `0` | Distance at which child glass elements begin to merge. |
|
|
104
|
+
|
|
105
|
+
## Platform Support Check
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { isLiquidGlassSupported } from "@k-workspace/react-native-liquidglass-view";
|
|
109
|
+
|
|
110
|
+
if (isLiquidGlassSupported) {
|
|
111
|
+
// iOS 26+ — real glass
|
|
112
|
+
} else {
|
|
113
|
+
// Fallback rendered automatically
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Platform Support
|
|
118
|
+
|
|
119
|
+
| Platform | Status |
|
|
120
|
+
| -------- | ----------------------------------- |
|
|
121
|
+
| iOS 26+ | Full native `UIGlassEffect` support |
|
|
122
|
+
| iOS < 26 | Translucent fallback view |
|
|
123
|
+
| Android | In progress |
|
|
124
|
+
|
|
125
|
+
> Android native support is currently being developed. In the meantime, a translucent `<View>` fallback is rendered automatically so your layout remains intact on all platforms.
|
|
126
|
+
|
|
127
|
+
## How It Works
|
|
128
|
+
|
|
129
|
+
- On **iOS 26+**, the native view creates a `UIVisualEffectView` backed by `UIGlassEffect` (or `UIGlassContainerEffect` for the container). It adds:
|
|
130
|
+
- A subtle white edge border for glass definition
|
|
131
|
+
- A specular top-highlight gradient simulating overhead light
|
|
132
|
+
- Dual-layer shadows (contact + elevation) for depth
|
|
133
|
+
- Optional spring press animation with haptic feedback
|
|
134
|
+
- Optional fade-in entrance animation
|
|
135
|
+
|
|
136
|
+
- On **older iOS** and **Android**, a styled `<View>` with translucent background, border, and shadow is rendered as a fallback.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
@objcMembers
|
|
4
|
+
@objc(BNGlassContainerView)
|
|
5
|
+
public class BNGlassContainerView: UIView {
|
|
6
|
+
|
|
7
|
+
private var containerEffectView: UIVisualEffectView?
|
|
8
|
+
|
|
9
|
+
// MARK: - React Native Props
|
|
10
|
+
|
|
11
|
+
@objc public var spacing: CGFloat = 0 {
|
|
12
|
+
didSet { rebuildEffect() }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// MARK: - Init
|
|
16
|
+
|
|
17
|
+
public override init(frame: CGRect) {
|
|
18
|
+
super.init(frame: frame)
|
|
19
|
+
commonInit()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public required init?(coder: NSCoder) {
|
|
23
|
+
super.init(coder: coder)
|
|
24
|
+
commonInit()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private func commonInit() {
|
|
28
|
+
backgroundColor = .clear
|
|
29
|
+
clipsToBounds = false
|
|
30
|
+
buildEffect()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// MARK: - Container Effect
|
|
34
|
+
|
|
35
|
+
private func buildEffect() {
|
|
36
|
+
let reactChildren: [UIView] = subviews
|
|
37
|
+
.filter { $0 !== containerEffectView }
|
|
38
|
+
|
|
39
|
+
containerEffectView?.removeFromSuperview()
|
|
40
|
+
containerEffectView = nil
|
|
41
|
+
|
|
42
|
+
#if compiler(>=6.2)
|
|
43
|
+
if #available(iOS 26.0, *) {
|
|
44
|
+
guard NSClassFromString("UIGlassContainerEffect") != nil else { return }
|
|
45
|
+
|
|
46
|
+
let containerEffect = UIGlassContainerEffect()
|
|
47
|
+
containerEffect.spacing = spacing
|
|
48
|
+
|
|
49
|
+
let ev = UIVisualEffectView(effect: containerEffect)
|
|
50
|
+
ev.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
51
|
+
ev.frame = bounds
|
|
52
|
+
insertSubview(ev, at: 0)
|
|
53
|
+
containerEffectView = ev
|
|
54
|
+
|
|
55
|
+
for child in reactChildren {
|
|
56
|
+
insertSubview(child, aboveSubview: ev)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
#endif
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private func rebuildEffect() {
|
|
63
|
+
buildEffect()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// MARK: - Layout
|
|
67
|
+
|
|
68
|
+
public override func layoutSubviews() {
|
|
69
|
+
super.layoutSubviews()
|
|
70
|
+
containerEffectView?.frame = bounds
|
|
71
|
+
if let ev = containerEffectView {
|
|
72
|
+
sendSubviewToBack(ev)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// MARK: - React Native Subview Management
|
|
77
|
+
|
|
78
|
+
@objc public func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
|
|
79
|
+
if let ev = containerEffectView {
|
|
80
|
+
insertSubview(subview, aboveSubview: ev)
|
|
81
|
+
} else {
|
|
82
|
+
insertSubview(subview, at: atIndex)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@objc public func removeReactSubview(_ subview: UIView!) {
|
|
87
|
+
subview?.removeFromSuperview()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// MARK: - Effect Style Enum
|
|
4
|
+
|
|
5
|
+
@objc public enum BNGlassEffectStyle: Int {
|
|
6
|
+
case regular
|
|
7
|
+
case clear
|
|
8
|
+
|
|
9
|
+
#if compiler(>=6.2)
|
|
10
|
+
@available(iOS 26.0, *)
|
|
11
|
+
var uiGlassStyle: UIGlassEffect.Style {
|
|
12
|
+
switch self {
|
|
13
|
+
case .regular: return .regular
|
|
14
|
+
case .clear: return .clear
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
#endif
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// MARK: - BNGlassView
|
|
21
|
+
|
|
22
|
+
@objcMembers
|
|
23
|
+
@objc(BNGlassView)
|
|
24
|
+
public class BNGlassView: UIView {
|
|
25
|
+
|
|
26
|
+
// MARK: - Sublayers & Subviews
|
|
27
|
+
|
|
28
|
+
private var glassEffectView: UIVisualEffectView?
|
|
29
|
+
private var contactShadowLayer: CALayer?
|
|
30
|
+
private var elevationShadowLayer: CALayer?
|
|
31
|
+
private var specularLayer: CAGradientLayer?
|
|
32
|
+
private lazy var feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
|
33
|
+
|
|
34
|
+
// MARK: - React Native Props
|
|
35
|
+
|
|
36
|
+
@objc public var cornerRadius: CGFloat = 16 {
|
|
37
|
+
didSet { applyCornerRadius() }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@objc public var glassTintColor: UIColor? {
|
|
41
|
+
didSet { rebuildEffect() }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@objc public var effect: NSString = "regular" {
|
|
45
|
+
didSet { rebuildEffect() }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@objc public var interactive: Bool = true {
|
|
49
|
+
didSet { rebuildEffect() }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@objc public var colorScheme: NSString? {
|
|
53
|
+
didSet { applyColorScheme() }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@objc public var shadowIntensity: CGFloat = 0.15 {
|
|
57
|
+
didSet { updateShadows() }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@objc public var enablePressAnimation: Bool = false
|
|
61
|
+
|
|
62
|
+
@objc public var enableEntrance: Bool = false
|
|
63
|
+
|
|
64
|
+
// MARK: - Private State
|
|
65
|
+
|
|
66
|
+
private var isPressed = false
|
|
67
|
+
private var hasAnimatedEntrance = false
|
|
68
|
+
private var currentEffectStyle: BNGlassEffectStyle = .regular
|
|
69
|
+
|
|
70
|
+
// MARK: - Init
|
|
71
|
+
|
|
72
|
+
public override init(frame: CGRect) {
|
|
73
|
+
super.init(frame: frame)
|
|
74
|
+
commonInit()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public required init?(coder: NSCoder) {
|
|
78
|
+
super.init(coder: coder)
|
|
79
|
+
commonInit()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func commonInit() {
|
|
83
|
+
backgroundColor = .clear
|
|
84
|
+
clipsToBounds = false
|
|
85
|
+
layer.cornerCurve = .continuous
|
|
86
|
+
setupShadowLayers()
|
|
87
|
+
buildEffect()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MARK: - Glass Effect
|
|
91
|
+
|
|
92
|
+
private func buildEffect() {
|
|
93
|
+
currentEffectStyle = (effect as String) == "clear" ? .clear : .regular
|
|
94
|
+
|
|
95
|
+
let reactChildren: [UIView] = subviews
|
|
96
|
+
.filter { $0 !== glassEffectView }
|
|
97
|
+
|
|
98
|
+
specularLayer?.removeFromSuperlayer()
|
|
99
|
+
specularLayer = nil
|
|
100
|
+
glassEffectView?.removeFromSuperview()
|
|
101
|
+
glassEffectView = nil
|
|
102
|
+
|
|
103
|
+
#if compiler(>=6.2)
|
|
104
|
+
if #available(iOS 26.0, *) {
|
|
105
|
+
guard let glassEffectClass = NSClassFromString("UIGlassEffect") as? NSObject.Type else { return }
|
|
106
|
+
guard glassEffectClass.responds(to: Selector(("effectWithStyle:"))) else { return }
|
|
107
|
+
|
|
108
|
+
let glass = UIGlassEffect(style: currentEffectStyle.uiGlassStyle)
|
|
109
|
+
glass.isInteractive = interactive
|
|
110
|
+
|
|
111
|
+
if let tint = glassTintColor {
|
|
112
|
+
glass.tintColor = tint
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let ev = UIVisualEffectView(effect: glass)
|
|
116
|
+
ev.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
117
|
+
ev.frame = bounds
|
|
118
|
+
ev.clipsToBounds = true
|
|
119
|
+
ev.layer.cornerRadius = cornerRadius
|
|
120
|
+
ev.layer.cornerCurve = .continuous
|
|
121
|
+
|
|
122
|
+
ev.layer.borderWidth = 0.5
|
|
123
|
+
ev.layer.borderColor = UIColor.white.withAlphaComponent(0.3).cgColor
|
|
124
|
+
|
|
125
|
+
insertSubview(ev, at: 0)
|
|
126
|
+
glassEffectView = ev
|
|
127
|
+
|
|
128
|
+
let specular = CAGradientLayer()
|
|
129
|
+
specular.colors = [
|
|
130
|
+
UIColor.white.withAlphaComponent(0.15).cgColor,
|
|
131
|
+
UIColor.white.withAlphaComponent(0.0).cgColor,
|
|
132
|
+
]
|
|
133
|
+
specular.startPoint = CGPoint(x: 0.5, y: 0)
|
|
134
|
+
specular.endPoint = CGPoint(x: 0.5, y: 1)
|
|
135
|
+
specular.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 6)
|
|
136
|
+
specular.cornerRadius = cornerRadius
|
|
137
|
+
specular.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
138
|
+
ev.contentView.layer.addSublayer(specular)
|
|
139
|
+
specularLayer = specular
|
|
140
|
+
|
|
141
|
+
for child in reactChildren {
|
|
142
|
+
insertSubview(child, aboveSubview: ev)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
applyCornerRadius()
|
|
146
|
+
}
|
|
147
|
+
#endif
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private func rebuildEffect() {
|
|
151
|
+
buildEffect()
|
|
152
|
+
updateShadows()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// MARK: - Shadow System
|
|
156
|
+
|
|
157
|
+
private func setupShadowLayers() {
|
|
158
|
+
let contact = CALayer()
|
|
159
|
+
contact.shadowColor = UIColor.black.cgColor
|
|
160
|
+
contact.shadowOffset = CGSize(width: 0, height: 2)
|
|
161
|
+
contact.shadowRadius = 4
|
|
162
|
+
contact.shadowOpacity = 0.08
|
|
163
|
+
layer.insertSublayer(contact, at: 0)
|
|
164
|
+
contactShadowLayer = contact
|
|
165
|
+
|
|
166
|
+
let elevation = CALayer()
|
|
167
|
+
elevation.shadowColor = UIColor.black.cgColor
|
|
168
|
+
elevation.shadowOffset = CGSize(width: 0, height: 12)
|
|
169
|
+
elevation.shadowRadius = 24
|
|
170
|
+
elevation.shadowOpacity = Float(shadowIntensity)
|
|
171
|
+
layer.insertSublayer(elevation, at: 0)
|
|
172
|
+
elevationShadowLayer = elevation
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private func updateShadows() {
|
|
176
|
+
guard bounds.width > 0, bounds.height > 0 else { return }
|
|
177
|
+
|
|
178
|
+
let shadowPath = UIBezierPath(
|
|
179
|
+
roundedRect: bounds,
|
|
180
|
+
cornerRadius: cornerRadius
|
|
181
|
+
).cgPath
|
|
182
|
+
|
|
183
|
+
contactShadowLayer?.shadowPath = shadowPath
|
|
184
|
+
contactShadowLayer?.frame = bounds
|
|
185
|
+
|
|
186
|
+
elevationShadowLayer?.shadowPath = shadowPath
|
|
187
|
+
elevationShadowLayer?.frame = bounds
|
|
188
|
+
elevationShadowLayer?.shadowOpacity = Float(shadowIntensity)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// MARK: - Corner Radius
|
|
192
|
+
|
|
193
|
+
private func applyCornerRadius() {
|
|
194
|
+
layer.cornerRadius = cornerRadius
|
|
195
|
+
glassEffectView?.layer.cornerRadius = cornerRadius
|
|
196
|
+
specularLayer?.cornerRadius = cornerRadius
|
|
197
|
+
updateShadows()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// MARK: - Color Scheme
|
|
201
|
+
|
|
202
|
+
private func applyColorScheme() {
|
|
203
|
+
guard let scheme = colorScheme as String? else {
|
|
204
|
+
overrideUserInterfaceStyle = .unspecified
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
switch scheme {
|
|
208
|
+
case "dark": overrideUserInterfaceStyle = .dark
|
|
209
|
+
case "light": overrideUserInterfaceStyle = .light
|
|
210
|
+
default: overrideUserInterfaceStyle = .unspecified
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// MARK: - Touch Animation
|
|
215
|
+
|
|
216
|
+
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
217
|
+
super.touchesBegan(touches, with: event)
|
|
218
|
+
guard enablePressAnimation, !isPressed else { return }
|
|
219
|
+
isPressed = true
|
|
220
|
+
feedbackGenerator.impactOccurred()
|
|
221
|
+
animatePress(pressed: true)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
225
|
+
super.touchesEnded(touches, with: event)
|
|
226
|
+
guard enablePressAnimation, isPressed else { return }
|
|
227
|
+
isPressed = false
|
|
228
|
+
animatePress(pressed: false)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
232
|
+
super.touchesCancelled(touches, with: event)
|
|
233
|
+
guard enablePressAnimation, isPressed else { return }
|
|
234
|
+
isPressed = false
|
|
235
|
+
animatePress(pressed: false)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private func animatePress(pressed: Bool) {
|
|
239
|
+
let scale: CGFloat = pressed ? 0.97 : 1.0
|
|
240
|
+
let elevationOpacity: Float = pressed
|
|
241
|
+
? Float(shadowIntensity * 0.4)
|
|
242
|
+
: Float(shadowIntensity)
|
|
243
|
+
let contactOpacity: Float = pressed ? 0.12 : 0.08
|
|
244
|
+
|
|
245
|
+
UIView.animate(
|
|
246
|
+
withDuration: 0.4,
|
|
247
|
+
delay: 0,
|
|
248
|
+
usingSpringWithDamping: pressed ? 0.65 : 0.5,
|
|
249
|
+
initialSpringVelocity: pressed ? 0 : 0.8,
|
|
250
|
+
options: [.allowUserInteraction, .beginFromCurrentState]
|
|
251
|
+
) {
|
|
252
|
+
self.transform = CGAffineTransform(scaleX: scale, y: scale)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let shadowAnim = CABasicAnimation(keyPath: "shadowOpacity")
|
|
256
|
+
shadowAnim.toValue = elevationOpacity
|
|
257
|
+
shadowAnim.duration = 0.3
|
|
258
|
+
shadowAnim.fillMode = .forwards
|
|
259
|
+
shadowAnim.isRemovedOnCompletion = false
|
|
260
|
+
elevationShadowLayer?.add(shadowAnim, forKey: "shadowOpacity")
|
|
261
|
+
|
|
262
|
+
let contactAnim = CABasicAnimation(keyPath: "shadowOpacity")
|
|
263
|
+
contactAnim.toValue = contactOpacity
|
|
264
|
+
contactAnim.duration = 0.3
|
|
265
|
+
contactAnim.fillMode = .forwards
|
|
266
|
+
contactAnim.isRemovedOnCompletion = false
|
|
267
|
+
contactShadowLayer?.add(contactAnim, forKey: "shadowOpacity")
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// MARK: - Entrance Animation
|
|
271
|
+
|
|
272
|
+
public override func didMoveToWindow() {
|
|
273
|
+
super.didMoveToWindow()
|
|
274
|
+
guard enableEntrance, !hasAnimatedEntrance, window != nil else { return }
|
|
275
|
+
hasAnimatedEntrance = true
|
|
276
|
+
|
|
277
|
+
alpha = 0
|
|
278
|
+
transform = CGAffineTransform(translationX: 0, y: 8)
|
|
279
|
+
|
|
280
|
+
UIView.animate(
|
|
281
|
+
withDuration: 0.6,
|
|
282
|
+
delay: 0,
|
|
283
|
+
usingSpringWithDamping: 0.7,
|
|
284
|
+
initialSpringVelocity: 0,
|
|
285
|
+
options: [.allowUserInteraction]
|
|
286
|
+
) {
|
|
287
|
+
self.alpha = 1
|
|
288
|
+
self.transform = .identity
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// MARK: - Layout
|
|
293
|
+
|
|
294
|
+
public override func layoutSubviews() {
|
|
295
|
+
super.layoutSubviews()
|
|
296
|
+
glassEffectView?.frame = bounds
|
|
297
|
+
specularLayer?.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 6)
|
|
298
|
+
|
|
299
|
+
if let ev = glassEffectView {
|
|
300
|
+
sendSubviewToBack(ev)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
updateShadows()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// MARK: - React Native Subview Management
|
|
307
|
+
|
|
308
|
+
@objc public func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
|
|
309
|
+
if let ev = glassEffectView {
|
|
310
|
+
insertSubview(subview, aboveSubview: ev)
|
|
311
|
+
} else {
|
|
312
|
+
insertSubview(subview, at: atIndex)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@objc public func removeReactSubview(_ subview: UIView!) {
|
|
317
|
+
subview?.removeFromSuperview()
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#import <React/RCTViewManager.h>
|
|
2
|
+
#import "react_native_liquidGlass_view-Swift.h"
|
|
3
|
+
|
|
4
|
+
@interface LiquidGlassContainerManager : RCTViewManager
|
|
5
|
+
@end
|
|
6
|
+
|
|
7
|
+
@implementation LiquidGlassContainerManager
|
|
8
|
+
|
|
9
|
+
RCT_EXPORT_MODULE(RNLiquidGlassContainer)
|
|
10
|
+
|
|
11
|
+
- (UIView *)view {
|
|
12
|
+
return [[BNGlassContainerView alloc] init];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
RCT_EXPORT_VIEW_PROPERTY(spacing, CGFloat)
|
|
16
|
+
|
|
17
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
18
|
+
return YES;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#import <React/RCTViewManager.h>
|
|
2
|
+
#import <React/RCTConvert.h>
|
|
3
|
+
#import "react_native_liquidGlass_view-Swift.h"
|
|
4
|
+
|
|
5
|
+
@interface LiquidGlassViewManager : RCTViewManager
|
|
6
|
+
@end
|
|
7
|
+
|
|
8
|
+
@implementation LiquidGlassViewManager
|
|
9
|
+
|
|
10
|
+
RCT_EXPORT_MODULE(RNLiquidGlass)
|
|
11
|
+
|
|
12
|
+
- (UIView *)view {
|
|
13
|
+
return [[BNGlassView alloc] init];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
RCT_EXPORT_VIEW_PROPERTY(cornerRadius, CGFloat)
|
|
17
|
+
RCT_EXPORT_VIEW_PROPERTY(effect, NSString)
|
|
18
|
+
RCT_EXPORT_VIEW_PROPERTY(interactive, BOOL)
|
|
19
|
+
RCT_EXPORT_VIEW_PROPERTY(colorScheme, NSString)
|
|
20
|
+
|
|
21
|
+
// Map JS "tintColor" to the native "glassTintColor" property
|
|
22
|
+
// to avoid conflicting with UIView's built-in tintColor.
|
|
23
|
+
RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, BNGlassView)
|
|
24
|
+
{
|
|
25
|
+
view.glassTintColor = json ? [RCTConvert UIColor:json] : nil;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
RCT_EXPORT_VIEW_PROPERTY(shadowIntensity, CGFloat)
|
|
29
|
+
RCT_EXPORT_VIEW_PROPERTY(enablePressAnimation, BOOL)
|
|
30
|
+
RCT_EXPORT_VIEW_PROPERTY(enableEntrance, BOOL)
|
|
31
|
+
|
|
32
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
33
|
+
return YES;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
export interface LiquidGlassProps {
|
|
4
|
+
style?: StyleProp<ViewStyle>;
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Blends a semi-transparent color over the glass surface.
|
|
8
|
+
* Accepts any React Native color string (e.g. "rgba(255,0,0,0.2)").
|
|
9
|
+
*/
|
|
10
|
+
tintColor?: string;
|
|
11
|
+
/**
|
|
12
|
+
* When false, touch events pass through the glass to views below.
|
|
13
|
+
* @default true
|
|
14
|
+
*/
|
|
15
|
+
interactive?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Force the glass material to render in a specific colour scheme
|
|
18
|
+
* regardless of the system setting.
|
|
19
|
+
*/
|
|
20
|
+
colorScheme?: "light" | "dark";
|
|
21
|
+
/**
|
|
22
|
+
* Visual weight of the glass material.
|
|
23
|
+
* - "regular" — full glass (default)
|
|
24
|
+
* - "clear" — lighter, more transparent glass
|
|
25
|
+
* @default "regular"
|
|
26
|
+
*/
|
|
27
|
+
effect?: "regular" | "clear";
|
|
28
|
+
/**
|
|
29
|
+
* Corner radius applied to the glass surface.
|
|
30
|
+
* Uses continuous corner curve for Apple-style rounded corners.
|
|
31
|
+
* @default 16
|
|
32
|
+
*/
|
|
33
|
+
cornerRadius?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Controls the opacity of the main elevation shadow.
|
|
36
|
+
* Range: 0 (no shadow) to 1 (fully opaque shadow).
|
|
37
|
+
* @default 0.15
|
|
38
|
+
*/
|
|
39
|
+
shadowIntensity?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Enables a spring press-in/release scale animation with haptic feedback.
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
enablePressAnimation?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Enables a fade+translate entrance animation when the view first appears.
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
enableEntrance?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface LiquidGlassContainerProps {
|
|
52
|
+
style?: StyleProp<ViewStyle>;
|
|
53
|
+
children?: React.ReactNode;
|
|
54
|
+
/**
|
|
55
|
+
* The distance between child glass elements at which they begin to merge.
|
|
56
|
+
* @default 0
|
|
57
|
+
*/
|
|
58
|
+
spacing?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const isLiquidGlassSupported: boolean;
|
|
61
|
+
export declare const NativeLiquidGlassView: import("react-native").HostComponent<LiquidGlassProps> | null;
|
|
62
|
+
export declare const NativeLiquidGlassContainerView: import("react-native").HostComponent<LiquidGlassContainerProps> | null;
|
|
63
|
+
//# sourceMappingURL=NativeLiquidGlass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeLiquidGlass.d.ts","sourceRoot":"","sources":["../src/NativeLiquidGlass.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAGL,SAAS,EACT,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAC7B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,sBAAsB,SACsC,CAAC;AAE1E,eAAO,MAAM,qBAAqB,+DAE1B,CAAC;AAET,eAAO,MAAM,8BAA8B,wEAEnC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Platform, requireNativeComponent, } from "react-native";
|
|
2
|
+
export const isLiquidGlassSupported = Platform.OS === "ios" && parseInt(Platform.Version, 10) >= 26;
|
|
3
|
+
export const NativeLiquidGlassView = isLiquidGlassSupported
|
|
4
|
+
? requireNativeComponent("RNLiquidGlass")
|
|
5
|
+
: null;
|
|
6
|
+
export const NativeLiquidGlassContainerView = isLiquidGlassSupported
|
|
7
|
+
? requireNativeComponent("RNLiquidGlassContainer")
|
|
8
|
+
: null;
|
|
9
|
+
//# sourceMappingURL=NativeLiquidGlass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeLiquidGlass.js","sourceRoot":"","sources":["../src/NativeLiquidGlass.ts"],"names":[],"mappings":"AACA,OAAO,EACL,QAAQ,EACR,sBAAsB,GAGvB,MAAM,cAAc,CAAC;AA6DtB,MAAM,CAAC,MAAM,sBAAsB,GACjC,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAiB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;AAE1E,MAAM,CAAC,MAAM,qBAAqB,GAAG,sBAAsB;IACzD,CAAC,CAAC,sBAAsB,CAAmB,eAAe,CAAC;IAC3D,CAAC,CAAC,IAAI,CAAC;AAET,MAAM,CAAC,MAAM,8BAA8B,GAAG,sBAAsB;IAClE,CAAC,CAAC,sBAAsB,CAA4B,wBAAwB,CAAC;IAC7E,CAAC,CAAC,IAAI,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import { isLiquidGlassSupported } from "./NativeLiquidGlass";
|
|
4
|
+
import type { LiquidGlassProps, LiquidGlassContainerProps } from "./NativeLiquidGlass";
|
|
5
|
+
export type { LiquidGlassProps, LiquidGlassContainerProps };
|
|
6
|
+
export { isLiquidGlassSupported };
|
|
7
|
+
export interface GlassCardProps extends Pick<LiquidGlassProps, "tintColor" | "colorScheme" | "effect" | "cornerRadius" | "shadowIntensity" | "enablePressAnimation" | "enableEntrance"> {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
}
|
|
11
|
+
export declare const GlassCard: React.FC<GlassCardProps>;
|
|
12
|
+
export interface GlassViewProps extends LiquidGlassProps {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export declare const GlassView: React.FC<GlassViewProps>;
|
|
16
|
+
export interface GlassContainerProps extends LiquidGlassContainerProps {
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export declare const GlassContainer: React.FC<GlassContainerProps>;
|
|
20
|
+
export default GlassCard;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAoB,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EACL,sBAAsB,EAGvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACV,gBAAgB,EAChB,yBAAyB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAIlC,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,gBAAgB,EACd,WAAW,GACX,aAAa,GACb,QAAQ,GACR,cAAc,GACd,iBAAiB,GACjB,sBAAsB,GACtB,gBAAgB,CACnB;IACC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAiC9C,CAAC;AAIF,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACtD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAmC9C,CAAC;AAIF,MAAM,WAAW,mBAAoB,SAAQ,yBAAyB;IACpE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAcxD,CAAC;AAeF,eAAe,SAAS,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { isLiquidGlassSupported, NativeLiquidGlassView, NativeLiquidGlassContainerView, } from "./NativeLiquidGlass";
|
|
4
|
+
export { isLiquidGlassSupported };
|
|
5
|
+
export const GlassCard = ({ children, style, tintColor, colorScheme, effect = "clear", cornerRadius = 16, shadowIntensity, enablePressAnimation, enableEntrance, }) => {
|
|
6
|
+
if (!isLiquidGlassSupported || !NativeLiquidGlassView) {
|
|
7
|
+
return (_jsx(View, { style: [styles.fallback, { borderRadius: cornerRadius }, style], children: children }));
|
|
8
|
+
}
|
|
9
|
+
return (_jsx(NativeLiquidGlassView, { style: style, tintColor: tintColor, colorScheme: colorScheme, effect: effect, cornerRadius: cornerRadius, shadowIntensity: shadowIntensity, enablePressAnimation: enablePressAnimation, enableEntrance: enableEntrance, children: children }));
|
|
10
|
+
};
|
|
11
|
+
export const GlassView = ({ children, style, tintColor, interactive, colorScheme, effect = "clear", cornerRadius = 16, shadowIntensity, enablePressAnimation, enableEntrance, }) => {
|
|
12
|
+
if (!isLiquidGlassSupported || !NativeLiquidGlassView) {
|
|
13
|
+
return (_jsx(View, { style: [styles.fallback, { borderRadius: cornerRadius }, style], children: children }));
|
|
14
|
+
}
|
|
15
|
+
return (_jsx(NativeLiquidGlassView, { style: [{ borderRadius: cornerRadius }, style], tintColor: tintColor, colorScheme: colorScheme, interactive: interactive, effect: effect, cornerRadius: cornerRadius, shadowIntensity: shadowIntensity, enablePressAnimation: enablePressAnimation, enableEntrance: enableEntrance, children: children }));
|
|
16
|
+
};
|
|
17
|
+
export const GlassContainer = ({ children, style, spacing = 0, }) => {
|
|
18
|
+
if (!isLiquidGlassSupported || !NativeLiquidGlassContainerView) {
|
|
19
|
+
return _jsx(View, { style: style, children: children });
|
|
20
|
+
}
|
|
21
|
+
return (_jsx(NativeLiquidGlassContainerView, { style: style, spacing: spacing, children: children }));
|
|
22
|
+
};
|
|
23
|
+
const styles = StyleSheet.create({
|
|
24
|
+
fallback: {
|
|
25
|
+
backgroundColor: "rgba(225, 234, 240, 0.45)",
|
|
26
|
+
borderWidth: 0.5,
|
|
27
|
+
borderColor: "rgba(255, 255, 255, 0.4)",
|
|
28
|
+
shadowColor: "#000D2E",
|
|
29
|
+
shadowOffset: { width: 0, height: 12 },
|
|
30
|
+
shadowOpacity: 0.1,
|
|
31
|
+
shadowRadius: 24,
|
|
32
|
+
elevation: 18,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
export default GlassCard;
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@k-workspace/react-native-liquidglass-view",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "iOS 26 Liquid Glass effect for React Native — UIGlassEffect with configurable styles, shadows, press animations, and entrance transitions.",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"react-native": "src/index",
|
|
8
|
+
"source": "src/index",
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"lib",
|
|
12
|
+
"ios",
|
|
13
|
+
"react-native-liquidGlass-view.podspec",
|
|
14
|
+
"react-native.config.js",
|
|
15
|
+
"!**/__tests__"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc --project tsconfig.build.json",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"react-native",
|
|
23
|
+
"ios",
|
|
24
|
+
"liquid-glass",
|
|
25
|
+
"glass-effect",
|
|
26
|
+
"ios26",
|
|
27
|
+
"uiglasseffect",
|
|
28
|
+
"blur",
|
|
29
|
+
"glassmorphism",
|
|
30
|
+
"apple",
|
|
31
|
+
"native-module"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/techytypes/react-native-liquidGlass-view.git"
|
|
36
|
+
},
|
|
37
|
+
"author": "techytypes",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/techytypes/react-native-liquidGlass-view/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/techytypes/react-native-liquidGlass-view#readme",
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": ">=18.0.0",
|
|
45
|
+
"react-native": ">=0.73.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/react": "^18.0.0",
|
|
49
|
+
"react": "^18.0.0",
|
|
50
|
+
"react-native": "^0.84.0",
|
|
51
|
+
"typescript": "^5.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'react-native-liquidGlass-view'
|
|
3
|
+
s.version = '0.1.0'
|
|
4
|
+
s.summary = 'iOS 26 Liquid Glass effect for React Native'
|
|
5
|
+
s.description = 'Native UIGlassEffect wrapper for React Native with configurable styles, shadows, press animations, entrance transitions, and glass container merging.'
|
|
6
|
+
s.author = 'techytypes'
|
|
7
|
+
s.homepage = 'https://github.com/techytypes/react-native-liquidGlass-view'
|
|
8
|
+
s.license = { :type => 'MIT' }
|
|
9
|
+
s.platforms = { :ios => '15.1' }
|
|
10
|
+
s.source = { :git => 'https://github.com/techytypes/react-native-liquidGlass-view.git', :tag => s.version.to_s }
|
|
11
|
+
s.static_framework = true
|
|
12
|
+
|
|
13
|
+
s.dependency 'React-Core'
|
|
14
|
+
|
|
15
|
+
s.pod_target_xcconfig = {
|
|
16
|
+
'DEFINES_MODULE' => 'YES',
|
|
17
|
+
'SWIFT_VERSION' => '5.0',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
s.source_files = 'ios/**/*.{swift,h,m}'
|
|
21
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
requireNativeComponent,
|
|
5
|
+
StyleProp,
|
|
6
|
+
ViewStyle,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
|
|
9
|
+
export interface LiquidGlassProps {
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Blends a semi-transparent color over the glass surface.
|
|
14
|
+
* Accepts any React Native color string (e.g. "rgba(255,0,0,0.2)").
|
|
15
|
+
*/
|
|
16
|
+
tintColor?: string;
|
|
17
|
+
/**
|
|
18
|
+
* When false, touch events pass through the glass to views below.
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
interactive?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Force the glass material to render in a specific colour scheme
|
|
24
|
+
* regardless of the system setting.
|
|
25
|
+
*/
|
|
26
|
+
colorScheme?: "light" | "dark";
|
|
27
|
+
/**
|
|
28
|
+
* Visual weight of the glass material.
|
|
29
|
+
* - "regular" — full glass (default)
|
|
30
|
+
* - "clear" — lighter, more transparent glass
|
|
31
|
+
* @default "regular"
|
|
32
|
+
*/
|
|
33
|
+
effect?: "regular" | "clear";
|
|
34
|
+
/**
|
|
35
|
+
* Corner radius applied to the glass surface.
|
|
36
|
+
* Uses continuous corner curve for Apple-style rounded corners.
|
|
37
|
+
* @default 16
|
|
38
|
+
*/
|
|
39
|
+
cornerRadius?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Controls the opacity of the main elevation shadow.
|
|
42
|
+
* Range: 0 (no shadow) to 1 (fully opaque shadow).
|
|
43
|
+
* @default 0.15
|
|
44
|
+
*/
|
|
45
|
+
shadowIntensity?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Enables a spring press-in/release scale animation with haptic feedback.
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
enablePressAnimation?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Enables a fade+translate entrance animation when the view first appears.
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
enableEntrance?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface LiquidGlassContainerProps {
|
|
59
|
+
style?: StyleProp<ViewStyle>;
|
|
60
|
+
children?: React.ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* The distance between child glass elements at which they begin to merge.
|
|
63
|
+
* @default 0
|
|
64
|
+
*/
|
|
65
|
+
spacing?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const isLiquidGlassSupported =
|
|
69
|
+
Platform.OS === "ios" && parseInt(Platform.Version as string, 10) >= 26;
|
|
70
|
+
|
|
71
|
+
export const NativeLiquidGlassView = isLiquidGlassSupported
|
|
72
|
+
? requireNativeComponent<LiquidGlassProps>("RNLiquidGlass")
|
|
73
|
+
: null;
|
|
74
|
+
|
|
75
|
+
export const NativeLiquidGlassContainerView = isLiquidGlassSupported
|
|
76
|
+
? requireNativeComponent<LiquidGlassContainerProps>("RNLiquidGlassContainer")
|
|
77
|
+
: null;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleSheet, View, StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
isLiquidGlassSupported,
|
|
5
|
+
NativeLiquidGlassView,
|
|
6
|
+
NativeLiquidGlassContainerView,
|
|
7
|
+
} from "./NativeLiquidGlass";
|
|
8
|
+
import type {
|
|
9
|
+
LiquidGlassProps,
|
|
10
|
+
LiquidGlassContainerProps,
|
|
11
|
+
} from "./NativeLiquidGlass";
|
|
12
|
+
|
|
13
|
+
export type { LiquidGlassProps, LiquidGlassContainerProps };
|
|
14
|
+
export { isLiquidGlassSupported };
|
|
15
|
+
|
|
16
|
+
// GlassCard — opinionated card wrapper with sensible defaults
|
|
17
|
+
|
|
18
|
+
export interface GlassCardProps extends Pick<
|
|
19
|
+
LiquidGlassProps,
|
|
20
|
+
| "tintColor"
|
|
21
|
+
| "colorScheme"
|
|
22
|
+
| "effect"
|
|
23
|
+
| "cornerRadius"
|
|
24
|
+
| "shadowIntensity"
|
|
25
|
+
| "enablePressAnimation"
|
|
26
|
+
| "enableEntrance"
|
|
27
|
+
> {
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
style?: StyleProp<ViewStyle>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const GlassCard: React.FC<GlassCardProps> = ({
|
|
33
|
+
children,
|
|
34
|
+
style,
|
|
35
|
+
tintColor,
|
|
36
|
+
colorScheme,
|
|
37
|
+
effect = "clear",
|
|
38
|
+
cornerRadius = 16,
|
|
39
|
+
shadowIntensity,
|
|
40
|
+
enablePressAnimation,
|
|
41
|
+
enableEntrance,
|
|
42
|
+
}) => {
|
|
43
|
+
if (!isLiquidGlassSupported || !NativeLiquidGlassView) {
|
|
44
|
+
return (
|
|
45
|
+
<View style={[styles.fallback, { borderRadius: cornerRadius }, style]}>
|
|
46
|
+
{children}
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<NativeLiquidGlassView
|
|
53
|
+
style={style}
|
|
54
|
+
tintColor={tintColor}
|
|
55
|
+
colorScheme={colorScheme}
|
|
56
|
+
effect={effect}
|
|
57
|
+
cornerRadius={cornerRadius}
|
|
58
|
+
shadowIntensity={shadowIntensity}
|
|
59
|
+
enablePressAnimation={enablePressAnimation}
|
|
60
|
+
enableEntrance={enableEntrance}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</NativeLiquidGlassView>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// GlassView — lower-level view exposing all native props
|
|
68
|
+
|
|
69
|
+
export interface GlassViewProps extends LiquidGlassProps {
|
|
70
|
+
children: React.ReactNode;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const GlassView: React.FC<GlassViewProps> = ({
|
|
74
|
+
children,
|
|
75
|
+
style,
|
|
76
|
+
tintColor,
|
|
77
|
+
interactive,
|
|
78
|
+
colorScheme,
|
|
79
|
+
effect = "clear",
|
|
80
|
+
cornerRadius = 16,
|
|
81
|
+
shadowIntensity,
|
|
82
|
+
enablePressAnimation,
|
|
83
|
+
enableEntrance,
|
|
84
|
+
}) => {
|
|
85
|
+
if (!isLiquidGlassSupported || !NativeLiquidGlassView) {
|
|
86
|
+
return (
|
|
87
|
+
<View style={[styles.fallback, { borderRadius: cornerRadius }, style]}>
|
|
88
|
+
{children}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<NativeLiquidGlassView
|
|
95
|
+
style={[{ borderRadius: cornerRadius }, style]}
|
|
96
|
+
tintColor={tintColor}
|
|
97
|
+
colorScheme={colorScheme}
|
|
98
|
+
interactive={interactive}
|
|
99
|
+
effect={effect}
|
|
100
|
+
cornerRadius={cornerRadius}
|
|
101
|
+
shadowIntensity={shadowIntensity}
|
|
102
|
+
enablePressAnimation={enablePressAnimation}
|
|
103
|
+
enableEntrance={enableEntrance}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</NativeLiquidGlassView>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// GlassContainer — groups child glass views so they merge together
|
|
111
|
+
|
|
112
|
+
export interface GlassContainerProps extends LiquidGlassContainerProps {
|
|
113
|
+
children: React.ReactNode;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const GlassContainer: React.FC<GlassContainerProps> = ({
|
|
117
|
+
children,
|
|
118
|
+
style,
|
|
119
|
+
spacing = 0,
|
|
120
|
+
}) => {
|
|
121
|
+
if (!isLiquidGlassSupported || !NativeLiquidGlassContainerView) {
|
|
122
|
+
return <View style={style}>{children}</View>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<NativeLiquidGlassContainerView style={style} spacing={spacing}>
|
|
127
|
+
{children}
|
|
128
|
+
</NativeLiquidGlassContainerView>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const styles = StyleSheet.create({
|
|
133
|
+
fallback: {
|
|
134
|
+
backgroundColor: "rgba(225, 234, 240, 0.45)",
|
|
135
|
+
borderWidth: 0.5,
|
|
136
|
+
borderColor: "rgba(255, 255, 255, 0.4)",
|
|
137
|
+
shadowColor: "#000D2E",
|
|
138
|
+
shadowOffset: { width: 0, height: 12 },
|
|
139
|
+
shadowOpacity: 0.1,
|
|
140
|
+
shadowRadius: 24,
|
|
141
|
+
elevation: 18,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export default GlassCard;
|