While investigating a significant KIF test-suite slowdown when moving from an iOS 17.x Simulator runtime to iOS 18+, I traced a significant part of the regression to the UICollectionView accessibility enumeration path in UIView-KIFAdditions.m.
On iOS 18+, KIF scrolls the collection view to materialize offscreen cells, calls layoutIfNeeded, restores the original contentOffset, calls layoutIfNeeded again, and then unconditionally runs the run loop for CELL_SCROLL_DELAY_STABILIZATION (50ms).
In large test suites this fixed delay accumulates significantly because the accessibility traversal path can be executed thousands of times.
Relevant code
[collectionView setContentOffset:initialPosition.origin];
[collectionView layoutIfNeeded];
CFRunLoopRunInMode(
UIApplicationCurrentRunMode,
CELL_SCROLL_DELAY_STABILIZATION,
false
);
Profiling
In one full-suite profiling run:
UICollectionView scan completions: 2134
requested stabilization time: 106.7s
actual CFRunLoop time: ~113s
The final 50ms stabilization accounted for the vast majority of the run-loop time measured inside the affected accessibility traversal path.
The same final stabilization also exists on pre-iOS-18 runtimes, so it cannot by itself explain the entire iOS 17.x → iOS 18+ runtime regression.
However, with the iOS 18+ scrolling-based UICollectionView traversal it becomes a significant avoidable fixed cost in a hot path and can accumulate substantially across a large test suite.
Background
This path appears to originate from the iOS 18 UICollectionView changes introduced around #1307, where direct data-source cell enumeration became unsafe because of UIKit dequeue validation.
Follow-up changes such as #1312, #1313 and #1319 added fallback scrolling, nil-cell handling and retry logic.
The proposed change does not remove any of those safeguards:
- real
UICollectionView scrolling remains unchanged;
layoutIfNeeded remains unchanged;
- the
10 * 10ms retry loop remains unchanged;
scrollToItemAtIndexPath fallback remains unchanged;
- nil-cell handling remains unchanged;
- behavior below iOS 18 remains unchanged.
Performance validation
As an experiment, I first removed the final 50ms stabilization wait completely on iOS 18+, while preserving the existing materialization, retry and fallback logic.
For common tests that passed in both runs, the observed iOS 17.2 → iOS 26.2 regression decreased from:
Control: +12.47%
Fixed: +6.20%
Two additional A/B runs directly on iOS 26.2 showed a reduction in the summed duration of common passing tests of:
Run 1: -13.34%
Run 2: -15.09%
These results confirmed that the final stabilization wait is a significant source of avoidable cost in this hot path.
However, correctness validation showed that removing it unconditionally was unnecessarily aggressive, so the proposed production-safe change is narrower.
Proposed change
Only skip the final 50ms stabilization on iOS 18+ when the accessibility traversal did not actually change the collection view's contentOffset.
BOOL needsContentOffsetRestore = !CGPointEqualToPoint(
collectionView.contentOffset,
initialPosition.origin
);
[collectionView setContentOffset:initialPosition.origin];
[collectionView layoutIfNeeded];
if (@available(iOS 18, *)) {
if (needsContentOffsetRestore) {
CFRunLoopRunInMode(
UIApplicationCurrentRunMode,
CELL_SCROLL_DELAY_STABILIZATION,
false
);
}
} else {
CFRunLoopRunInMode(
UIApplicationCurrentRunMode,
CELL_SCROLL_DELAY_STABILIZATION,
false
);
}
In additional instrumentation on iOS 26.2, an actual contentOffset restore was needed in only:
118 / 4391 exhausted scans (~2.7%)
This keeps the existing stabilization when the accessibility traversal actually moved the collection view, while avoiding the fixed delay for the majority of scans where no restoration is necessary.
Correctness validation
During full-suite validation, the more aggressive version that removed the final wait completely exposed a timing-sensitive test.
The test performed a direct UIView.tap() and then immediately waited for the next UI state. It had previously been implicitly benefiting from the incidental run-loop yield inside the UICollectionView accessibility traversal.
The failure was not related to incorrect UICollectionView restoration.
Adding the normal KIF post-action synchronization:
view.tap()
tester().waitForAnimationsToFinish()
made the test pass 100/100.
This suggests that removing the incidental delay can expose tests that implicitly depend on unrelated run-loop timing, but those cases can be synchronized explicitly rather than relying on a delay inside accessibility enumeration.
Why this seems safe
The iOS 18+ path still performs the existing non-animated scrolling and synchronous layoutIfNeeded.
All cell materialization retry/fallback mechanisms remain intact.
The final stabilization is also preserved whenever KIF actually needs to restore the collection view's position.
The change only avoids the fixed 50ms delay when the traversal did not change contentOffset in the first place.
This also seems consistent with the reasoning in #1307, where layoutIfNeeded was preferred over spinning the run loop for cell materialization because it was faster and avoided unnecessary redraw work.
Question
Is there a known correctness or stability reason for keeping the final 50ms run-loop delay on iOS 18+ when the accessibility traversal did not change contentOffset?
If not, I can open a PR with the minimal conditional change above and the performance/correctness validation data.
While investigating a significant KIF test-suite slowdown when moving from an iOS 17.x Simulator runtime to iOS 18+, I traced a significant part of the regression to the
UICollectionViewaccessibility enumeration path inUIView-KIFAdditions.m.On iOS 18+, KIF scrolls the collection view to materialize offscreen cells, calls
layoutIfNeeded, restores the originalcontentOffset, callslayoutIfNeededagain, and then unconditionally runs the run loop forCELL_SCROLL_DELAY_STABILIZATION(50ms).In large test suites this fixed delay accumulates significantly because the accessibility traversal path can be executed thousands of times.
Relevant code
Profiling
In one full-suite profiling run:
The final 50ms stabilization accounted for the vast majority of the run-loop time measured inside the affected accessibility traversal path.
The same final stabilization also exists on pre-iOS-18 runtimes, so it cannot by itself explain the entire iOS 17.x → iOS 18+ runtime regression.
However, with the iOS 18+ scrolling-based
UICollectionViewtraversal it becomes a significant avoidable fixed cost in a hot path and can accumulate substantially across a large test suite.Background
This path appears to originate from the iOS 18
UICollectionViewchanges introduced around #1307, where direct data-source cell enumeration became unsafe because of UIKit dequeue validation.Follow-up changes such as #1312, #1313 and #1319 added fallback scrolling, nil-cell handling and retry logic.
The proposed change does not remove any of those safeguards:
UICollectionViewscrolling remains unchanged;layoutIfNeededremains unchanged;10 * 10msretry loop remains unchanged;scrollToItemAtIndexPathfallback remains unchanged;Performance validation
As an experiment, I first removed the final 50ms stabilization wait completely on iOS 18+, while preserving the existing materialization, retry and fallback logic.
For common tests that passed in both runs, the observed iOS 17.2 → iOS 26.2 regression decreased from:
Two additional A/B runs directly on iOS 26.2 showed a reduction in the summed duration of common passing tests of:
These results confirmed that the final stabilization wait is a significant source of avoidable cost in this hot path.
However, correctness validation showed that removing it unconditionally was unnecessarily aggressive, so the proposed production-safe change is narrower.
Proposed change
Only skip the final 50ms stabilization on iOS 18+ when the accessibility traversal did not actually change the collection view's
contentOffset.In additional instrumentation on iOS 26.2, an actual
contentOffsetrestore was needed in only:This keeps the existing stabilization when the accessibility traversal actually moved the collection view, while avoiding the fixed delay for the majority of scans where no restoration is necessary.
Correctness validation
During full-suite validation, the more aggressive version that removed the final wait completely exposed a timing-sensitive test.
The test performed a direct
UIView.tap()and then immediately waited for the next UI state. It had previously been implicitly benefiting from the incidental run-loop yield inside theUICollectionViewaccessibility traversal.The failure was not related to incorrect
UICollectionViewrestoration.Adding the normal KIF post-action synchronization:
made the test pass 100/100.
This suggests that removing the incidental delay can expose tests that implicitly depend on unrelated run-loop timing, but those cases can be synchronized explicitly rather than relying on a delay inside accessibility enumeration.
Why this seems safe
The iOS 18+ path still performs the existing non-animated scrolling and synchronous
layoutIfNeeded.All cell materialization retry/fallback mechanisms remain intact.
The final stabilization is also preserved whenever KIF actually needs to restore the collection view's position.
The change only avoids the fixed 50ms delay when the traversal did not change
contentOffsetin the first place.This also seems consistent with the reasoning in #1307, where
layoutIfNeededwas preferred over spinning the run loop for cell materialization because it was faster and avoided unnecessary redraw work.Question
Is there a known correctness or stability reason for keeping the final 50ms run-loop delay on iOS 18+ when the accessibility traversal did not change
contentOffset?If not, I can open a PR with the minimal conditional change above and the performance/correctness validation data.