Skip to content

Cancel in-progress reorders when the list is torn down - #630

Draft
elizrus wants to merge 1 commit into
square:mainfrom
elizrus:erussell/pcm-2178-cancel-reorder-on-teardown
Draft

Cancel in-progress reorders when the list is torn down#630
elizrus wants to merge 1 commit into
square:mainfrom
elizrus:erussell/pcm-2178-cancel-reorder-on-teardown

Conversation

@elizrus

@elizrus elizrus commented Aug 5, 2026

Copy link
Copy Markdown

Summary

Fixes a crash when a list is torn down while an interactive reorder is still in progress — for example, the user navigates away (taps a nav-bar shortcut, pops the screen) while still holding a drag.

A reorder gesture drives a native UICollectionView interactive-movement session, which UIKit expects to be resolved with endInteractiveMovement() or cancelInteractiveMovement(). Today nothing ends that session when the list leaves the window or is deallocated:

  • deinit only nils collectionView.delegate/dataSource.
  • didMoveToWindow() does nothing when window == nil.
  • The only caller of cancelAllInProgressReorders() is the content-diff-apply path (guarded by hasInProgressReorders, and annotated as experimental/"will likely crash").

So the interactive-movement session outlives the content it was started against. A later content update or layout pass then reads a now-stale index and traps:

Swift runtime failure: Index out of range
Array.subscript.getter
ListLayoutContent.item(at:)
closure #1 in ListLayoutContent.move(from:to:)   // via CollectionViewLayout.invalidateLayout(with:)

(also observed via CollectionViewLayout.positionForItem(at:)ListView.setPresentationStateItemPositions().)

Change

Cancel any in-progress reorder while the data source and layout are still in sync:

  • In didMoveToWindow() when the list leaves the window (the navigate-away-mid-drag case).
  • In deinit as a backstop.

Both are guarded by hasInProgressReorders, so they are no-ops in the common case. hasInProgressReorders is relaxed from private to internal so tests can assert on it.

Testing

Adds ListView.ReorderTeardownTests covering both teardown paths (window removal and deinit). Verified the reorder state is cleared on window exit and that deallocation with a live reorder does not crash.

Reported from Square Point of Sale: app crashes when a payment type is dragged between sections in Payment settings and a navigation-bar shortcut is tapped mid-drag.

Checklist

  • Ensure any public-facing changes are reflected in the changelog. Include them in the Main section.

A reorder gesture drives a native UICollectionView interactive-movement
session that expects to be resolved with endInteractiveMovement or
cancelInteractiveMovement. If the list leaves the window or is deallocated
while a drag is still held — for example, the user navigates away mid-drag —
nothing ends that session. It outlives the content it was started against,
so a later content update or layout pass reads a now-stale index and traps
in the layout (Index out of range in ListLayoutContent.item(at:)).

Cancel any in-progress reorder in didMoveToWindow when leaving the window,
and in deinit, while the data source and layout are still in sync. Both are
guarded by hasInProgressReorders so they are no-ops in the common case.

Add regression tests covering the window-removal and deinit teardown paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant