feat(TextureIGListKitExtensions): add performUpdatesWithFallback with pre-flight section-count guard#14
Merged
Conversation
… pre-flight section-count guard
Adds ListAdapter.performUpdatesWithFallback(animated:completion:) and an
async overload. Performs a pre-flight check that
UICollectionView.numberOfSections matches IGListAdapter.objects().count; on
divergence, falls back to reloadData(completion:) instead of letting
IGListKit raise NSInternalInconsistencyException from
IGListBatchUpdateTransaction._didDiff:onBackground: with the message
The UICollectionView's section count (N) didn't match the
IGListAdapter's count (M), so we can't performBatchUpdates.
Falling back to reloadData.
(IGListKit's message text says "falling back to reloadData", but IGAssert
raises an NSException in release builds — the documented fallback never
executes.)
Scope:
Catches already-divergent states where the collection view was reset
externally (reloadData from another path, data source reassignment, view
re-layout that lost its section count) while the adapter still holds the
previous applied snapshot.
Out of scope:
Does not prevent the same exception when triggered by a concurrent
in-flight diff. IGListKit captures the sectionMap inside its background
diff queue; a second performUpdates that mutates the data source between
snapshot and apply still raises from _didDiff:onBackground:. Callers must
serialize concurrent loads on the same adapter — this guard is a
defence-in-depth layer, not a replacement for caller-side serialization.
Tests:
IGListAdapterBridgeTests adds two regression tests:
- performUpdatesWithFallback_appliesDiffs_whenSectionCountsMatch —
happy path, ensures the guard does not regress normal consecutive
diffs.
- performUpdatesWithFallback_isNoOp_whenNoCollectionViewAttached —
verifies the nil-collectionView fall-through to performUpdates.
The fallback branch is not exercised by a dedicated test because the
IGListKit + AsyncDisplayKit API contract intentionally prevents callers
from constructing the divergent precondition: dataSource swaps crash on
interop selectors, IGListAdapter is not open for subclassing outside its
defining module, and method-swizzling UICollectionView.numberOfSections
leaks across the surrounding regression tests. The branch is one if
statement and is verified by inspection; an inline comment in the test
file documents this rationale.
Observability:
Divergence events are logged via os_log at .error level under
subsystem: TextureIGListKitExtensions, category: iglistkit-guard.
Consumers can filter for these in Console.app or pipe them into their
structured-logging stack to measure how often the fallback fires in
production.
39941eb to
7e80a2d
Compare
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.
Summary
Adds
ListAdapter.performUpdatesWithFallback(animated:completion:)(plus an async overload) that performs a pre-flight checkUICollectionView.numberOfSections == adapter.objects().countand falls back toreloadData(completion:)on divergence, instead of letting IGListKit raiseNSInternalInconsistencyExceptionfromIGListBatchUpdateTransaction._didDiff:onBackground:with(IGListKit's message text says "falling back to reloadData", but
IGAssertraises an NSException in release builds — the documented fallback never executes.)Scope
Catches already-divergent states, where the collection view was reset externally (
reloadDatafrom another path, data source reassignment, view re-layout that lost its section count) while the adapter still holds the previous applied snapshot.Out of scope
Does not prevent the same exception when triggered by a concurrent in-flight diff: IGListKit captures the
sectionMapinside its background diff queue, and a secondperformUpdatesthat mutates the data source between snapshot and apply still raises from_didDiff:onBackground:. Callers must serialize concurrent loads on the same adapter — this guard is a defence-in-depth layer, not a replacement for caller-side serialization.Also out of scope:
ASCollectionInvalidUpdateExceptionraised fromASDataController.mm:598when a changeSet validates against a data source already mutated by a subsequent load. That race requires caller-side serialization of the load → performUpdates cycle.Tests
IGListAdapterBridgeTestsadds:performUpdatesWithFallback_appliesDiffs_whenSectionCountsMatch— happy path, ensures the guard does not regress normal consecutive diffs.performUpdatesWithFallback_isNoOp_whenNoCollectionViewAttached— verifies the nil-collectionView fall-through toperformUpdates.The fallback branch is not exercised by a dedicated test because the IGListKit + AsyncDisplayKit API contract intentionally prevents callers from constructing the divergent precondition (dataSource swaps crash on interop selectors;
IGListAdapteris not open for subclassing outside its defining module; method-swizzlingUICollectionView.numberOfSectionswould leak across the surrounding regression tests). The branch is oneifstatement and is verified by inspection; an inline comment in the test file documents this rationale.Local run on iPhone 17 / iOS 26.4.1 simulator: 9/9 tests passed (7 existing regression + 2 new).
Observability
Divergence events are logged via
os_logat.errorlevel undersubsystem: TextureIGListKitExtensions,category: iglistkit-guard. Consumers can filter for these in Console.app or pipe them into their structured-logging stack to measure how often the fallback fires in production.Test plan
ListAdapter) — no breaking changes to existing API.3.3.5) once merged so SPM consumers can pin the new revision.