Fix CollectionViewLayout delegate lifetime and ListView teardown crash#613
Closed
RoyalPineapple wants to merge 5 commits intomainfrom
Closed
Fix CollectionViewLayout delegate lifetime and ListView teardown crash#613RoyalPineapple wants to merge 5 commits intomainfrom
RoyalPineapple wants to merge 5 commits intomainfrom
Conversation
Hold the layout delegate weakly instead of unowned so the layout no longer crashes if the delegate is deallocated before the layout. prepare() and the deferred reorder flush now snapshot the delegate locally; LayoutManager's layout-swap path uses listableInternalFatal if the previous layout's delegate is missing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
During ListView.deinit, the embedded UICollectionView could remain in the view hierarchy briefly while its internal UICollectionViewData was being torn down. If a layout pass (e.g. from layoutBelowIfNeeded) hit the window during this window, UIKit would call layoutSubviews on the collection view, accessing the already-freed UICollectionViewData and crashing in objc_loadWeakRetained with a garbage weak ref. Explicitly removing the collection view from its superview in deinit ensures UIKit won't attempt to lay it out after ListView begins deallocation.
Covers the OperationQueue.main path in sendEndQueuingEditsAfterDelay, which was the original crash motivating the weak-delegate fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The removeFromSuperview in deinit fires too late — the ListView may still be retained while KIF traverses the view hierarchy during test teardown. Moving cleanup to didMoveToWindow(nil) detaches the collection view as soon as the ListView leaves the window, preventing UICollectionViewData from accessing a corrupted weak reference during layout.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ListView.deinithas two lifecycle issues that can cause crashes during view hierarchy teardown:1. CollectionViewLayout delegate
CollectionViewLayoutheld its delegate asunowned let. If the delegate is deallocated before the layout (e.g. during a layout swap or via a deferred dispatch fromsendEndQueuingEditsAfterDelay), any access traps. Concretely,OperationQueue.main.addOperation { self.delegate.listViewShouldEndQueueingEditsForReorder() }schedules work for a later runloop turn — if the owningListViewdeallocates between scheduling and dispatch, the closure trapped on the danglingunownedreference.2. UICollectionView remaining in view hierarchy during deallocation (crash fix)
The embedded
UICollectionViewremained in the view hierarchy duringListView.deinit. If a layout pass (layoutBelowIfNeeded) hit the window during this interval, UIKit would calllayoutSubviewson the collection view, which internally accessesUICollectionViewData._updateItemCounts. TheUICollectionViewDatastores a weak reference to its parentUICollectionViewat offset +0x8. During the deallocation cascade, this weak reference's memory was already freed and reused, causingobjc_loadWeakRetainedto read garbage values and SIGSEGV.How we discovered this
While integrating Market's accessibility label deferral feature into ios-register, we added
.accessibility(identifier:)to tab bar items, which wraps each item in an additionalCombinableView(Blueprint'sAccessibilitySetter). This extra view layer caused additionallayoutSubviews→updateAccessibility()calls during KIF's recursiveaccessibilityElementMatchingBlocktraversal. The extra layout passes triggeredlayoutBelowIfNeededon the window, which forced layout on aListView's embeddedUICollectionViewwhose internalUICollectionViewDatawas mid-deallocation.Evidence:
objc_loadWeakRetainedcalled fromUICollectionViewData._updateItemCountsx2register containedtype metadata for CollectionViewLayout— confirming it was a ListableUI collection viewUICollectionViewData.initWithCollectionView:layout:confirmed the weak ref at offset +0x8self.collectionView.removeFromSuperview()inListView.deiniteliminated the crash (0/3 on same config, 0/1 in CI)Fix
Commit 1: Delegate lifetime
Change
delegatefromunowned lettoprivate(set) weak var, thread the delegate through layout helpers as a parameter (so it stays strongly held for the duration of each call), capture it weakly in the deferredOperationQueueclosure, and guardprepare()with an early return when the delegate has gone.Commit 2: Collection view removal
Add
self.collectionView.removeFromSuperview()inListView.deinit. This ensures the collection view is immediately removed from the view hierarchy before the deallocation cascade reachesUICollectionViewData, solayoutBelowIfNeededwon't find it in the layer tree.Test
Adds two unit tests in
CollectionViewLayoutTests:test_prepare_whenDelegateHasBeenDeallocated— verifiesprepare()is safe after the delegate has been released.test_sendEndQueuingEditsAfterDelay_whenDelegateDeallocatesBeforeDispatch— exercises the original crash path: schedule the deferred operation, drop the delegate, then drain the main queue to confirm the closure no-ops instead of trapping.The
removeFromSuperviewfix was validated against the deterministic crash reproduction in ios-register's KIF test suite (no unit test — the crash is a UIKit-internal use-after-free that requires a window/layout pass to reproduce).Risk
Low. Both fixes target deallocation edge cases. In normal flows,
ListViewretains its delegate and is properly removed from the hierarchy before dealloc.Checklist