react-native-swipeable-actions 0.1.9 → 0.1.10-canary.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.
@@ -105,8 +105,12 @@ class SwipeableView(context: Context, appContext: AppContext) : ExpoView(context
105
105
  var actionsWidth: Float = 0f
106
106
  set(value) {
107
107
  val validated = maxOf(0f, value)
108
- if (field == validated) return
108
+ val previous = field
109
+ if (previous == validated) return
109
110
  field = validated
111
+ if (shouldSyncOpenLayoutForPropChange(previous, validated)) {
112
+ syncOpenStateToCurrentWidth()
113
+ }
110
114
  requestLayout()
111
115
  }
112
116
 
@@ -766,6 +770,28 @@ class SwipeableView(context: Context, appContext: AppContext) : ExpoView(context
766
770
  actions.translationX = actionsTranslateX
767
771
  }
768
772
 
773
+ private fun shouldSyncOpenLayoutForPropChange(oldValue: Float, newValue: Float): Boolean {
774
+ if (oldValue == newValue) return false
775
+ if (!isOpen || isDragging || isAnimating || isGestureActivated) return false
776
+ return newValue > 0f
777
+ }
778
+
779
+ private fun syncOpenStateToCurrentWidth() {
780
+ val targetX = if (isLeading) actionsWidthPx else -actionsWidthPx
781
+ currentTranslation = targetX
782
+ contentView?.translationX = targetX
783
+
784
+ if (actionsView != null) {
785
+ actionsView?.visibility = View.VISIBLE
786
+ actionsView?.translationX = 0f
787
+ } else {
788
+ onSwipeStart(emptyMap())
789
+ }
790
+
791
+ startLayoutGuard()
792
+ emitFinalProgress(1f, targetX)
793
+ }
794
+
769
795
  fun open(velocity: Float = 0f) {
770
796
  isOpen = true
771
797
  isAnimating = true
@@ -115,6 +115,11 @@ public class SwipeableView: ExpoView {
115
115
  // MARK: - State
116
116
  private var actionsView: UIView?
117
117
  private var contentView: UIView?
118
+ // Track React-managed children explicitly via Fabric's mount hooks.
119
+ // Using UIView.subviews is unsafe because UIKit/Expo/RN may insert helper
120
+ // views (clipping wrappers, focus indicators, etc.) that shift indices and
121
+ // cause us to transform the wrong UIView.
122
+ private var reactChildren: [UIView] = []
118
123
  private var currentTranslation: CGFloat = 0
119
124
  private var isOpen = false
120
125
  private var startOffset: CGFloat = 0
@@ -135,6 +140,9 @@ public class SwipeableView: ExpoView {
135
140
  actionsWidth = validated
136
141
  return
137
142
  }
143
+ if shouldSyncOpenLayoutForPropChange(oldValue: oldValue, newValue: validated) {
144
+ syncOpenStateToCurrentWidth()
145
+ }
138
146
  setNeedsLayout()
139
147
  }
140
148
  }
@@ -312,6 +320,18 @@ public class SwipeableView: ExpoView {
312
320
  currentAnimator = nil
313
321
  isAnimating = false
314
322
 
323
+ // Reset internal gesture/translation state so a re-attached view
324
+ // (FlashList recycle) does not re-apply a stale currentTranslation
325
+ // in the next layoutSubviews pass.
326
+ currentTranslation = 0
327
+ isOpen = false
328
+ pendingShouldOpen = false
329
+ startOffset = 0
330
+ lastEmittedProgress = -1
331
+ isDragging = false
332
+ isGestureActivated = false
333
+ gestureStartTranslation = 0
334
+
315
335
  // Reset layer properties to prevent corruption
316
336
  contentView?.transform = .identity
317
337
  contentView?.layer.zPosition = 0
@@ -469,38 +489,83 @@ public class SwipeableView: ExpoView {
469
489
  actionsView.transform = CGAffineTransform(translationX: actionsTranslateX, y: 0)
470
490
  }
471
491
 
492
+ private func shouldSyncOpenLayoutForPropChange(oldValue: CGFloat, newValue: CGFloat) -> Bool {
493
+ guard oldValue != newValue else { return false }
494
+ guard isOpen, !isDragging, !isAnimating, !isGestureActivated else { return false }
495
+ return newValue > 0
496
+ }
497
+
498
+ private func syncOpenStateToCurrentWidth() {
499
+ let targetX = isLeading ? actionsWidth : -actionsWidth
500
+ currentTranslation = targetX
501
+ contentView?.transform = CGAffineTransform(translationX: targetX, y: 0)
502
+
503
+ if actionsView != nil {
504
+ actionsView?.transform = .identity
505
+ } else {
506
+ onSwipeStart([:])
507
+ }
508
+
509
+ onSwipeProgress(["progress": 1.0, "translationX": targetX])
510
+ }
511
+
472
512
  // MARK: - View Lifecycle
473
513
 
474
- public override func didAddSubview(_ subview: UIView) {
475
- super.didAddSubview(subview)
514
+ // Track React-managed children only. Fabric guarantees this fires solely
515
+ // for views Fabric mounts (the JSX children) — never for UIKit helper
516
+ // views that UIView.subviews would otherwise include.
517
+ public override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
518
+ super.mountChildComponentView(childComponentView, index: index)
519
+ let clampedIndex = min(max(index, 0), reactChildren.count)
520
+ reactChildren.insert(childComponentView, at: clampedIndex)
521
+ refreshChildAssignments()
522
+ }
523
+
524
+ public override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
525
+ // Fabric recycles UIView instances. Before handing this child back, undo any
526
+ // transform / zPosition we may have applied — otherwise the next React
527
+ // component that receives this recycled UIView inherits our stale state and
528
+ // appears translated.
529
+ childComponentView.transform = .identity
530
+ childComponentView.layer.zPosition = 0
531
+
532
+ if index >= 0 && index < reactChildren.count && reactChildren[index] === childComponentView {
533
+ reactChildren.remove(at: index)
534
+ } else if let found = reactChildren.firstIndex(where: { $0 === childComponentView }) {
535
+ reactChildren.remove(at: found)
536
+ }
537
+
538
+ if childComponentView === contentView { contentView = nil }
539
+ if childComponentView === actionsView { actionsView = nil }
540
+
541
+ super.unmountChildComponentView(childComponentView, index: index)
542
+ refreshChildAssignments()
543
+ }
476
544
 
477
- let reactSubviews = subviews
545
+ private func refreshChildAssignments() {
478
546
  let wasActionsNil = actionsView == nil
479
547
  let wasContentNil = contentView == nil
480
548
 
481
- // Validate subview count before assignment
482
- switch reactSubviews.count {
549
+ switch reactChildren.count {
483
550
  case 0:
484
- // Should not happen - we just added a subview
485
551
  contentView = nil
486
552
  actionsView = nil
487
553
  case 1:
488
- contentView = reactSubviews[0]
554
+ contentView = reactChildren[0]
489
555
  contentView?.layer.zPosition = 1
490
556
  actionsView = nil
491
557
  case 2:
492
- actionsView = reactSubviews[0]
558
+ actionsView = reactChildren[0]
493
559
  actionsView?.layer.zPosition = -1
494
- contentView = reactSubviews[1]
560
+ contentView = reactChildren[1]
495
561
  contentView?.layer.zPosition = 1
496
562
  default:
497
- // More than 2 children - log warning but use first two
498
563
  #if DEBUG
499
- print("[SwipeableView] Warning: Expected 1-2 subviews, got \(reactSubviews.count)")
564
+ print("[SwipeableView] Warning: Expected 1-2 React children, got \(reactChildren.count)")
500
565
  #endif
501
- actionsView = reactSubviews[0]
566
+ actionsView = reactChildren[0]
502
567
  actionsView?.layer.zPosition = -1
503
- contentView = reactSubviews[1]
568
+ contentView = reactChildren[1]
504
569
  contentView?.layer.zPosition = 1
505
570
  }
506
571
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-swipeable-actions",
3
- "version": "0.1.9",
3
+ "version": "0.1.10-canary.0",
4
4
  "description": "High-performance native swipeable row component for React Native using Expo Modules",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",