@craft-native/ios 0.0.338 → 0.0.340

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.338",
3
+ "version": "0.0.340",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,7 +60,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
60
60
  /// The row this cell showed is gone; its views stay in the cell, and
61
61
  /// the next row it shows reuses them when it has the same shape.
62
62
  func recycle() {
63
- if let identity = representedIdentity { onRecycle?(identity) }
63
+ if let identity = representedIdentity, showsHostedView { onRecycle?(identity) }
64
64
  representedIdentity = nil
65
65
  onRecycle = nil
66
66
  }
@@ -81,6 +81,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
81
81
  // since moved to another cell is that cell's now, and pulling it
82
82
  // from there blanked a row on screen.
83
83
  if let shown = hostedView, shown !== view, shown.superview === contentView { shown.removeFromSuperview() }
84
+ NSLayoutConstraint.deactivate(hostedConstraints)
84
85
  hostedView = view
85
86
  view.translatesAutoresizingMaskIntoConstraints = false
86
87
  contentView.addSubview(view)
@@ -110,6 +111,8 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
110
111
  func releaseHostedView() {
111
112
  recycle()
112
113
  if let shown = hostedView, shown.superview === contentView { shown.removeFromSuperview() }
114
+ NSLayoutConstraint.deactivate(hostedConstraints)
115
+ hostedConstraints = []
113
116
  hostedView = nil
114
117
  }
115
118
 
@@ -195,8 +198,11 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
195
198
  private var endReachedThreshold = 0.1
196
199
  private var endReachedSignature: String?
197
200
  private var dataContentSignature = ""
201
+ private var dataOrdinals: [Int] = []
202
+ private var dataItemCount = 0
198
203
  private var columns = 1
199
204
  private var isApplyingSnapshot = false
205
+ private var snapshotNeedsReset = false
200
206
  private var pendingApply: (() -> Void)?
201
207
  private var pendingThemeRefresh = false
202
208
  private let nativeRefreshControl = UIRefreshControl()
@@ -224,6 +230,10 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
224
230
  backgroundColor = .clear
225
231
  alwaysBounceVertical = true
226
232
  delegate = self
233
+ // The renderer keeps one live view per keyed row. UIKit's speculative
234
+ // cell prefetch can otherwise move that view out of its visible cell.
235
+ // UICollectionView still recycles and self-sizes the visible window.
236
+ isPrefetchingEnabled = false
227
237
  keyboardTapGesture.cancelsTouchesInView = false
228
238
  keyboardTapGesture.delegate = self
229
239
  keyboardTapGesture.addTarget(self, action: #selector(keyboardTap))
@@ -382,13 +392,15 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
382
392
  let padding = nonNegative(raw?["padding"]) ?? 0
383
393
  let horizontal = nonNegative(raw?["paddingHorizontal"]) ?? padding
384
394
  let vertical = nonNegative(raw?["paddingVertical"]) ?? padding
385
- contentInset = UIEdgeInsets(
395
+ let inset = UIEdgeInsets(
386
396
  top: nonNegative(raw?["paddingTop"]) ?? vertical,
387
397
  left: nonNegative(raw?["paddingLeft"]) ?? horizontal,
388
398
  bottom: nonNegative(raw?["paddingBottom"]) ?? vertical,
389
399
  right: nonNegative(raw?["paddingRight"]) ?? horizontal
390
400
  )
391
- scrollIndicatorInsets = contentInset
401
+ guard contentInset != inset else { return }
402
+ contentInset = inset
403
+ scrollIndicatorInsets = inset
392
404
  collectionViewLayout.invalidateLayout()
393
405
  }
394
406
 
@@ -479,7 +491,9 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
479
491
  self.removeItem = removeItem
480
492
  self.endReached = endReached
481
493
  self.endReachedThreshold = max(0, endReachedThreshold)
482
- self.columns = max(1, columns)
494
+ let nextColumns = max(1, columns)
495
+ let layoutChanged = self.columns != nextColumns || flowLayout.scrollDirection != (horizontal ? .horizontal : .vertical)
496
+ self.columns = nextColumns
483
497
 
484
498
  layoutIfNeeded()
485
499
  let oldItems = itemsByIdentity
@@ -496,6 +510,19 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
496
510
  return Item(identity: identity, node: node, signature: signature, isChrome: role != nil && role != "item")
497
511
  }
498
512
  if inverted { next.reverse() }
513
+ // Reapplying an identical keyed tree during a timer or badge update
514
+ // must not reconfigure cells, invalidate self-sizing, or reset a drag.
515
+ if !snapshotNeedsReset, !layoutChanged, next.count == items.count,
516
+ zip(next, items).allSatisfy({ $0.identity == $1.identity && $0.signature == $1.signature }) {
517
+ finishApplyingSnapshot()
518
+ return
519
+ }
520
+ snapshotNeedsReset = false
521
+ dataItemCount = 0
522
+ dataOrdinals = next.map { item in
523
+ if !item.isChrome { dataItemCount += 1 }
524
+ return dataItemCount - 1
525
+ }
499
526
  items = next
500
527
  itemsByIdentity = Dictionary(uniqueKeysWithValues: next.map { ($0.identity, $0) })
501
528
 
@@ -504,7 +531,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
504
531
  alwaysBounceVertical = !horizontal
505
532
  showsHorizontalScrollIndicator = horizontal
506
533
  showsVerticalScrollIndicator = !horizontal
507
- flowLayout.invalidateLayout()
534
+ if layoutChanged { flowLayout.invalidateLayout() }
508
535
 
509
536
  let nextIds = Set(itemsByIdentity.keys)
510
537
  for identity in oldItems.keys where !nextIds.contains(identity) { removeItem(identity) }
@@ -550,9 +577,12 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
550
577
  }
551
578
 
552
579
  func discardAll() {
580
+ snapshotNeedsReset = true
553
581
  visibleCells.compactMap { $0 as? Cell }.forEach { $0.releaseHostedView() }
554
582
  itemsByIdentity.keys.forEach { recycleItem?($0) }
555
583
  items = []
584
+ dataOrdinals = []
585
+ dataItemCount = 0
556
586
  itemsByIdentity = [:]
557
587
  renderItem = nil
558
588
  recycleItem = nil
@@ -638,11 +668,11 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
638
668
  guard !isApplyingSnapshot, pendingApply == nil,
639
669
  let endReached = endReached,
640
670
  let lastVisible = indexPathsForVisibleItems.map(\.item).max() else { return }
641
- let dataIndices = items.indices.filter { !items[$0].isChrome }
642
- guard !dataIndices.isEmpty,
643
- let lastDataOrdinal = dataIndices.lastIndex(where: { $0 <= lastVisible }) else { return }
644
- let remaining = dataIndices.count - lastDataOrdinal - 1
645
- let thresholdItems = max(1, Int(ceil(Double(dataIndices.count) * endReachedThreshold)))
671
+ guard dataItemCount > 0, dataOrdinals.indices.contains(lastVisible) else { return }
672
+ let lastDataOrdinal = dataOrdinals[lastVisible]
673
+ guard lastDataOrdinal >= 0 else { return }
674
+ let remaining = dataItemCount - lastDataOrdinal - 1
675
+ let thresholdItems = max(1, Int(ceil(Double(dataItemCount) * endReachedThreshold)))
646
676
  guard remaining <= thresholdItems else { return }
647
677
  let signature = dataContentSignature
648
678
  guard endReachedSignature != signature else { return }