react-native-swipeable-actions 0.1.10 → 0.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/SwipeableView.swift +58 -16
- package/package.json +1 -1
package/ios/SwipeableView.swift
CHANGED
|
@@ -66,8 +66,25 @@ public class SwipeableView: ExpoView {
|
|
|
66
66
|
registryQueue.async(flags: .barrier) { _viewRegistry[key] = WeakRef(view) }
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
private static func unregisterView(for key: String) {
|
|
70
|
-
|
|
69
|
+
private static func unregisterView(_ view: SwipeableView, for key: String) {
|
|
70
|
+
// Identity-check: a newer SwipeableView may have already taken over this
|
|
71
|
+
// key (FlashList remounts a new instance before the old one's deinit
|
|
72
|
+
// fires). Without this guard the old view's deinit blindly removes the
|
|
73
|
+
// key, leaving the new view orphaned in the registry — closeByKey for
|
|
74
|
+
// that key then returns nil and the visible row can't be closed.
|
|
75
|
+
//
|
|
76
|
+
// Capture identity as a *value* (ObjectIdentifier), never reference
|
|
77
|
+
// `view` inside the async closure. Callers include `deinit`, where
|
|
78
|
+
// `self` is mid-deallocation; strongly capturing it in an escaping
|
|
79
|
+
// closure produces a zombie reference and crashes (EXC_BAD_ACCESS) when
|
|
80
|
+
// the barrier finally runs after the object has been freed.
|
|
81
|
+
let identity = ObjectIdentifier(view)
|
|
82
|
+
registryQueue.async(flags: .barrier) {
|
|
83
|
+
if let registered = _viewRegistry[key]?.value,
|
|
84
|
+
ObjectIdentifier(registered) == identity {
|
|
85
|
+
_viewRegistry.removeValue(forKey: key)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
private static func clearOpenState(for key: String) {
|
|
@@ -88,7 +105,15 @@ public class SwipeableView: ExpoView {
|
|
|
88
105
|
|
|
89
106
|
static func closeByKey(_ key: String, animated: Bool = true) {
|
|
90
107
|
DispatchQueue.main.async {
|
|
91
|
-
getView(for: key)
|
|
108
|
+
if let view = getView(for: key) {
|
|
109
|
+
view.close(animated: animated)
|
|
110
|
+
} else {
|
|
111
|
+
// View was recycled away (or unregistered) before this close
|
|
112
|
+
// could run. Update the cache directly so a future cell binding
|
|
113
|
+
// to this key starts closed instead of inheriting a stale open
|
|
114
|
+
// state — see recyclingKey setter for the matching invariant.
|
|
115
|
+
setOpenState(for: key, isOpen: false)
|
|
116
|
+
}
|
|
92
117
|
}
|
|
93
118
|
}
|
|
94
119
|
|
|
@@ -229,10 +254,15 @@ public class SwipeableView: ExpoView {
|
|
|
229
254
|
let shouldOpen = recyclingKey.map { Self.getOpenState(for: $0) } ?? false
|
|
230
255
|
pendingShouldOpen = shouldOpen
|
|
231
256
|
|
|
232
|
-
//
|
|
257
|
+
// Unregister from the old key. We intentionally do NOT write back
|
|
258
|
+
// `isOpen` here: open() and close() already maintain the cache when
|
|
259
|
+
// the user actually triggers the state change. Writing the view's
|
|
260
|
+
// current `isOpen` at reassignment time captures stale state when a
|
|
261
|
+
// pending async close hasn't run yet, leaving the old key cached as
|
|
262
|
+
// open forever — the next cell binding to it would snap open
|
|
263
|
+
// without any user intent.
|
|
233
264
|
if let old = oldValue {
|
|
234
|
-
Self.
|
|
235
|
-
Self.unregisterView(for: old)
|
|
265
|
+
Self.unregisterView(self, for: old)
|
|
236
266
|
}
|
|
237
267
|
|
|
238
268
|
// Thread-safe register with new key
|
|
@@ -261,9 +291,6 @@ public class SwipeableView: ExpoView {
|
|
|
261
291
|
// Auto-close work item for tracking and cancellation
|
|
262
292
|
private var autoCloseWorkItem: DispatchWorkItem?
|
|
263
293
|
|
|
264
|
-
// Override onSwipeEnd state when autoClose triggers close() (emit "open" instead of "closed")
|
|
265
|
-
private var autoCloseEndEventOverride = false
|
|
266
|
-
|
|
267
294
|
// UIViewPropertyAnimator for interruptible animations (2.1, 2.2)
|
|
268
295
|
private var currentAnimator: UIViewPropertyAnimator?
|
|
269
296
|
|
|
@@ -307,7 +334,7 @@ public class SwipeableView: ExpoView {
|
|
|
307
334
|
removeGestureRecognizer(panGesture)
|
|
308
335
|
// Thread-safe deregister from view registry
|
|
309
336
|
if let key = recyclingKey {
|
|
310
|
-
Self.unregisterView(for: key)
|
|
337
|
+
Self.unregisterView(self, for: key)
|
|
311
338
|
}
|
|
312
339
|
}
|
|
313
340
|
|
|
@@ -340,7 +367,7 @@ public class SwipeableView: ExpoView {
|
|
|
340
367
|
|
|
341
368
|
// Unregister from static registry and clear cached state
|
|
342
369
|
if let key = recyclingKey {
|
|
343
|
-
Self.unregisterView(for: key)
|
|
370
|
+
Self.unregisterView(self, for: key)
|
|
344
371
|
Self.clearOpenState(for: key)
|
|
345
372
|
}
|
|
346
373
|
}
|
|
@@ -451,8 +478,7 @@ public class SwipeableView: ExpoView {
|
|
|
451
478
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: retryWorkItem)
|
|
452
479
|
} else if isOpen {
|
|
453
480
|
// Emit "open" state when autoClose completes - user's swipe intent was achieved
|
|
454
|
-
|
|
455
|
-
close()
|
|
481
|
+
close(autoCloseOverride: true)
|
|
456
482
|
}
|
|
457
483
|
}
|
|
458
484
|
|
|
@@ -516,12 +542,27 @@ public class SwipeableView: ExpoView {
|
|
|
516
542
|
// views that UIView.subviews would otherwise include.
|
|
517
543
|
public override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
|
|
518
544
|
super.mountChildComponentView(childComponentView, index: index)
|
|
545
|
+
// Incoming child may carry a stale transform/zPosition from Fabric recycling;
|
|
546
|
+
// reset it (refreshChildAssignments re-applies the open transform when open).
|
|
547
|
+
childComponentView.transform = .identity
|
|
548
|
+
childComponentView.layer.zPosition = 0
|
|
519
549
|
let clampedIndex = min(max(index, 0), reactChildren.count)
|
|
520
550
|
reactChildren.insert(childComponentView, at: clampedIndex)
|
|
521
551
|
refreshChildAssignments()
|
|
522
552
|
}
|
|
523
553
|
|
|
524
554
|
public override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
|
|
555
|
+
// Content recycled mid-animation: stop the animator before contentView is
|
|
556
|
+
// nil'd below, else it keeps writing a transform onto the pooled view.
|
|
557
|
+
// (Android skips this — RN zeroes translationX on recycle; port if needed.)
|
|
558
|
+
if childComponentView === contentView {
|
|
559
|
+
currentAnimator?.stopAnimation(true)
|
|
560
|
+
currentAnimator = nil
|
|
561
|
+
isAnimating = false
|
|
562
|
+
stopProgressUpdates()
|
|
563
|
+
cancelAutoClose() // a pending timer would fire on the reused view
|
|
564
|
+
}
|
|
565
|
+
|
|
525
566
|
// Fabric recycles UIView instances. Before handing this child back, undo any
|
|
526
567
|
// transform / zPosition we may have applied — otherwise the next React
|
|
527
568
|
// component that receives this recycled UIView inherits our stale state and
|
|
@@ -877,7 +918,9 @@ public class SwipeableView: ExpoView {
|
|
|
877
918
|
onSwipeProgress(["progress": 1.0, "translationX": targetX])
|
|
878
919
|
}
|
|
879
920
|
|
|
880
|
-
|
|
921
|
+
// autoCloseOverride is a value, not a sticky flag: an interrupted close (which
|
|
922
|
+
// never runs the completion) can't strand it and mislabel a later close as "open".
|
|
923
|
+
func close(animated: Bool = true, velocity: CGFloat = 0, autoCloseOverride: Bool = false) {
|
|
881
924
|
cancelAutoClose()
|
|
882
925
|
stopProgressUpdates()
|
|
883
926
|
|
|
@@ -921,8 +964,7 @@ public class SwipeableView: ExpoView {
|
|
|
921
964
|
self.actionsView?.transform = CGAffineTransform(translationX: actionsOffset, y: 0)
|
|
922
965
|
self.onSwipeProgress(["progress": 0.0, "translationX": 0.0])
|
|
923
966
|
// Emit "open" when autoClose completes - user's swipe intent was achieved
|
|
924
|
-
let endState =
|
|
925
|
-
self.autoCloseEndEventOverride = false
|
|
967
|
+
let endState = autoCloseOverride ? "open" : "closed"
|
|
926
968
|
self.onSwipeEnd(["state": endState])
|
|
927
969
|
|
|
928
970
|
// Restore first responder if it was lost during swipe (keyboard preservation)
|
package/package.json
CHANGED