Skip to content

Commit 9ad7399

Browse files
authored
fix: fix EXC_BAD_ACCESS in Skeleton on iOS (#6)
1 parent 5374d50 commit 9ad7399

File tree

11 files changed

+123
-30
lines changed

11 files changed

+123
-30
lines changed

native-modules/react-native-background-thread/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-background-thread",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"description": "react-native-background-thread",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-modules/react-native-check-biometric-auth-changed/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-check-biometric-auth-changed",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"description": "react-native-check-biometric-auth-changed",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-modules/react-native-cloud-kit-module/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-cloud-kit-module",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"description": "react-native-cloud-kit-module",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-modules/react-native-device-utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-device-utils",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"description": "react-native-device-utils",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-modules/react-native-get-random-values/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-get-random-values",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"description": "react-native-get-random-values",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-modules/react-native-keychain-module/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-keychain-module",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"description": "react-native-keychain-module",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-modules/react-native-lite-card/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-lite-card",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"description": "lite card",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-views/react-native-skeleton/android/src/main/java/com/margelo/nitro/skeleton/Skeleton.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,8 @@ class HybridSkeleton(val context: ThemedReactContext) : HybridSkeletonSpec() {
132132
DEFAULT_GRADIENT_COLORS[0]
133133
}
134134
}
135+
136+
override fun dispose() {
137+
stopShimmer()
138+
}
135139
}

native-views/react-native-skeleton/ios/Skeleton.swift

Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@ let DEFAULT_GRADIENT_COLORS: [UIColor] = [UIColor(red: 210.0/255.0, green: 210.0
66

77
class HybridSkeleton : HybridSkeletonSpec {
88

9-
// Shimmer layer
9+
// Shimmer layers
1010
private var shimmerLayer: CAGradientLayer?
11+
private var skeletonLayer: CALayer?
1112
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?
1217

1318
var shimmerGradientColors: [String]? {
1419
didSet {
15-
Task {
16-
customGradientColors = shimmerGradientColors?.map { hexStringToUIColor(hexColor: $0) }
17-
await MainActor.run {
18-
restartShimmer()
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()
2025
}
2126
}
2227
}
@@ -26,7 +31,11 @@ class HybridSkeleton : HybridSkeletonSpec {
2631

2732
var shimmerSpeed: Double? {
2833
didSet {
29-
restartShimmer()
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+
}
3039
}
3140
}
3241

@@ -35,22 +44,72 @@ class HybridSkeleton : HybridSkeletonSpec {
3544
setupView()
3645
}
3746

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+
3870
private func setupView() {
3971
view.clipsToBounds = true
4072

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+
4184
// 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 }
4387
self.startShimmer()
4488
}
4589
}
4690

4791
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+
4899
stopShimmer()
49100

50101
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
51109
// Retry after a short delay if bounds are not ready
52-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
53-
self.startShimmer()
110+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
111+
guard let self = self, self.isActive else { return }
112+
self.startShimmerInternal()
54113
}
55114
return
56115
}
@@ -59,26 +118,31 @@ class HybridSkeleton : HybridSkeletonSpec {
59118
let backgroundColor = colors[0].cgColor
60119
let highlightColor = colors[1].cgColor
61120

62-
let skeletonLayer = CALayer()
63-
skeletonLayer.backgroundColor = backgroundColor
64-
skeletonLayer.name = "SkeletonLayer"
65-
skeletonLayer.anchorPoint = .zero
66-
skeletonLayer.frame = view.bounds
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
67127

128+
// Create gradient layer for animation
68129
let gradientLayer = CAGradientLayer()
69130
gradientLayer.colors = [backgroundColor, highlightColor, backgroundColor]
70131
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
71132
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
72133
gradientLayer.frame = view.bounds
73134
gradientLayer.name = "ShimmerLayer"
74135

75-
view.layer.mask = skeletonLayer
76-
view.layer.addSublayer(skeletonLayer)
136+
// Set mask and add gradient layer (don't add skeleton layer as sublayer)
137+
view.layer.mask = newSkeletonLayer
77138
view.layer.addSublayer(gradientLayer)
78139
view.clipsToBounds = true
79140

141+
// Store references
142+
skeletonLayer = newSkeletonLayer
80143
shimmerLayer = gradientLayer
81144

145+
// Create animation
82146
let width = view.bounds.width
83147
let animation = CABasicAnimation(keyPath: "transform.translation.x")
84148
animation.duration = shimmerSpeed ?? 3
@@ -87,23 +151,48 @@ class HybridSkeleton : HybridSkeletonSpec {
87151
animation.repeatCount = .infinity
88152
animation.autoreverses = false
89153
animation.fillMode = CAMediaTimingFillMode.forwards
154+
animation.isRemovedOnCompletion = false // Keep animation when completed
90155

156+
// Use weak self pattern for potential future animation delegates/completion handlers
91157
gradientLayer.add(animation, forKey: "shimmerAnimation")
92158
}
93159

94160
func stopShimmer() {
95-
shimmerLayer?.removeAnimation(forKey: "shimmerAnimation")
161+
// Remove animation from shimmer layer
162+
shimmerLayer?.removeAllAnimations() // Remove all animations, more comprehensive
96163
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
97179
shimmerLayer = nil
180+
skeletonLayer = nil
98181
}
99182

100183
func afterUpdate() {
101-
restartShimmer()
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+
}
102189
}
103190

104191
func restartShimmer() {
192+
guard isActive else { return }
105193
stopShimmer()
106-
startShimmer()
194+
retryCount = 0 // Reset retry count on restart
195+
startShimmerInternal()
107196
}
108197

109198
func hexStringToUIColor(hexColor: String) -> UIColor {

native-views/react-native-skeleton/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-skeleton",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"description": "react-native-skeleton",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

0 commit comments

Comments
 (0)