@onekeyfe/react-native-skeleton 1.1.11 → 1.1.12
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/ios/Skeleton.swift
CHANGED
|
@@ -6,17 +6,22 @@ let DEFAULT_GRADIENT_COLORS: [UIColor] = [UIColor(red: 210.0/255.0, green: 210.0
|
|
|
6
6
|
|
|
7
7
|
class HybridSkeleton : HybridSkeletonSpec {
|
|
8
8
|
|
|
9
|
-
// Shimmer
|
|
9
|
+
// Shimmer layers
|
|
10
10
|
private var shimmerLayer: CAGradientLayer?
|
|
11
|
+
private var skeletonLayer: CALayer?
|
|
11
12
|
private var customGradientColors: [UIColor]?
|
|
13
|
+
private var isActive: Bool = true
|
|
14
|
+
private var retryCount: Int = 0
|
|
15
|
+
private let maxRetryCount: Int = 10
|
|
16
|
+
private var viewObserver: NSKeyValueObservation?
|
|
12
17
|
|
|
13
18
|
var shimmerGradientColors: [String]? {
|
|
14
19
|
didSet {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
guard isActive else { return }
|
|
21
|
+
DispatchQueue.main.async { [weak self] in
|
|
22
|
+
guard let self = self, self.isActive else { return }
|
|
23
|
+
self.customGradientColors = shimmerGradientColors?.map { self.hexStringToUIColor(hexColor: $0) }
|
|
24
|
+
self.restartShimmer()
|
|
20
25
|
}
|
|
21
26
|
}
|
|
22
27
|
}
|
|
@@ -26,7 +31,11 @@ class HybridSkeleton : HybridSkeletonSpec {
|
|
|
26
31
|
|
|
27
32
|
var shimmerSpeed: Double? {
|
|
28
33
|
didSet {
|
|
29
|
-
|
|
34
|
+
guard isActive else { return }
|
|
35
|
+
DispatchQueue.main.async { [weak self] in
|
|
36
|
+
guard let self = self, self.isActive else { return }
|
|
37
|
+
self.restartShimmer()
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
|
|
@@ -35,22 +44,72 @@ class HybridSkeleton : HybridSkeletonSpec {
|
|
|
35
44
|
setupView()
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
deinit {
|
|
48
|
+
// Mark as inactive to prevent any new operations
|
|
49
|
+
isActive = false
|
|
50
|
+
|
|
51
|
+
// Clean up observer
|
|
52
|
+
viewObserver?.invalidate()
|
|
53
|
+
viewObserver = nil
|
|
54
|
+
|
|
55
|
+
// Ensure all cleanup happens on main thread synchronously
|
|
56
|
+
if Thread.isMainThread {
|
|
57
|
+
stopShimmer()
|
|
58
|
+
} else {
|
|
59
|
+
DispatchQueue.main.sync {
|
|
60
|
+
self.stopShimmer()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Clear view reference and ensure no dangling animations
|
|
65
|
+
view.layer.removeAllAnimations()
|
|
66
|
+
view.layer.sublayers?.removeAll()
|
|
67
|
+
view.layer.mask = nil
|
|
68
|
+
}
|
|
69
|
+
|
|
38
70
|
private func setupView() {
|
|
39
71
|
view.clipsToBounds = true
|
|
40
72
|
|
|
73
|
+
// Observe view hierarchy changes for cleanup
|
|
74
|
+
viewObserver = view.observe(\.superview, options: [.new]) { [weak self] view, change in
|
|
75
|
+
guard let self = self else { return }
|
|
76
|
+
if change.newValue == nil {
|
|
77
|
+
// View was removed from hierarchy, clean up
|
|
78
|
+
DispatchQueue.main.async { [weak self] in
|
|
79
|
+
self?.stopShimmer()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
// Start animation when view is ready
|
|
42
|
-
DispatchQueue.main.async {
|
|
85
|
+
DispatchQueue.main.async { [weak self] in
|
|
86
|
+
guard let self = self, self.isActive else { return }
|
|
43
87
|
self.startShimmer()
|
|
44
88
|
}
|
|
45
89
|
}
|
|
46
90
|
|
|
47
91
|
func startShimmer() {
|
|
92
|
+
retryCount = 0 // Reset retry count when starting new shimmer
|
|
93
|
+
startShimmerInternal()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func startShimmerInternal() {
|
|
97
|
+
guard isActive else { return }
|
|
98
|
+
|
|
48
99
|
stopShimmer()
|
|
49
100
|
|
|
50
101
|
guard !view.bounds.isEmpty else {
|
|
102
|
+
// Check if we have exceeded max retry count
|
|
103
|
+
guard retryCount < maxRetryCount else {
|
|
104
|
+
print("⚠️ Skeleton shimmer: Max retry count reached. View bounds are still empty.")
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
retryCount += 1
|
|
51
109
|
// Retry after a short delay if bounds are not ready
|
|
52
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
53
|
-
self.
|
|
110
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
111
|
+
guard let self = self, self.isActive else { return }
|
|
112
|
+
self.startShimmerInternal()
|
|
54
113
|
}
|
|
55
114
|
return
|
|
56
115
|
}
|
|
@@ -59,12 +118,14 @@ class HybridSkeleton : HybridSkeletonSpec {
|
|
|
59
118
|
let backgroundColor = colors[0].cgColor
|
|
60
119
|
let highlightColor = colors[1].cgColor
|
|
61
120
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
121
|
+
// Create skeleton layer for masking
|
|
122
|
+
let newSkeletonLayer = CALayer()
|
|
123
|
+
newSkeletonLayer.backgroundColor = backgroundColor
|
|
124
|
+
newSkeletonLayer.name = "SkeletonLayer"
|
|
125
|
+
newSkeletonLayer.anchorPoint = .zero
|
|
126
|
+
newSkeletonLayer.frame = view.bounds
|
|
67
127
|
|
|
128
|
+
// Create gradient layer for animation
|
|
68
129
|
let gradientLayer = CAGradientLayer()
|
|
69
130
|
gradientLayer.colors = [backgroundColor, highlightColor, backgroundColor]
|
|
70
131
|
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
|
|
@@ -72,13 +133,16 @@ class HybridSkeleton : HybridSkeletonSpec {
|
|
|
72
133
|
gradientLayer.frame = view.bounds
|
|
73
134
|
gradientLayer.name = "ShimmerLayer"
|
|
74
135
|
|
|
75
|
-
|
|
76
|
-
view.layer.
|
|
136
|
+
// Set mask and add gradient layer (don't add skeleton layer as sublayer)
|
|
137
|
+
view.layer.mask = newSkeletonLayer
|
|
77
138
|
view.layer.addSublayer(gradientLayer)
|
|
78
139
|
view.clipsToBounds = true
|
|
79
140
|
|
|
141
|
+
// Store references
|
|
142
|
+
skeletonLayer = newSkeletonLayer
|
|
80
143
|
shimmerLayer = gradientLayer
|
|
81
144
|
|
|
145
|
+
// Create animation
|
|
82
146
|
let width = view.bounds.width
|
|
83
147
|
let animation = CABasicAnimation(keyPath: "transform.translation.x")
|
|
84
148
|
animation.duration = shimmerSpeed ?? 3
|
|
@@ -87,23 +151,48 @@ class HybridSkeleton : HybridSkeletonSpec {
|
|
|
87
151
|
animation.repeatCount = .infinity
|
|
88
152
|
animation.autoreverses = false
|
|
89
153
|
animation.fillMode = CAMediaTimingFillMode.forwards
|
|
154
|
+
animation.isRemovedOnCompletion = false // Keep animation when completed
|
|
90
155
|
|
|
156
|
+
// Use weak self pattern for potential future animation delegates/completion handlers
|
|
91
157
|
gradientLayer.add(animation, forKey: "shimmerAnimation")
|
|
92
158
|
}
|
|
93
159
|
|
|
94
160
|
func stopShimmer() {
|
|
95
|
-
|
|
161
|
+
// Remove animation from shimmer layer
|
|
162
|
+
shimmerLayer?.removeAllAnimations() // Remove all animations, more comprehensive
|
|
96
163
|
shimmerLayer?.removeFromSuperlayer()
|
|
164
|
+
|
|
165
|
+
// Clear mask reference before setting to nil
|
|
166
|
+
if view.layer.mask === skeletonLayer {
|
|
167
|
+
view.layer.mask = nil
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Clean up all sublayers that might be shimmer layers
|
|
171
|
+
view.layer.sublayers?.forEach { layer in
|
|
172
|
+
if layer.name == "ShimmerLayer" {
|
|
173
|
+
layer.removeAllAnimations()
|
|
174
|
+
layer.removeFromSuperlayer()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Nil out references
|
|
97
179
|
shimmerLayer = nil
|
|
180
|
+
skeletonLayer = nil
|
|
98
181
|
}
|
|
99
182
|
|
|
100
183
|
func afterUpdate() {
|
|
101
|
-
|
|
184
|
+
guard isActive else { return }
|
|
185
|
+
DispatchQueue.main.async { [weak self] in
|
|
186
|
+
guard let self = self, self.isActive else { return }
|
|
187
|
+
self.restartShimmer()
|
|
188
|
+
}
|
|
102
189
|
}
|
|
103
190
|
|
|
104
191
|
func restartShimmer() {
|
|
192
|
+
guard isActive else { return }
|
|
105
193
|
stopShimmer()
|
|
106
|
-
|
|
194
|
+
retryCount = 0 // Reset retry count on restart
|
|
195
|
+
startShimmerInternal()
|
|
107
196
|
}
|
|
108
197
|
|
|
109
198
|
func hexStringToUIColor(hexColor: String) -> UIColor {
|