[iOS] Fix incorrect FirstVisibleItemIndex reported by CollectionView.Scrolled after programmatic scroll#33719
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes an issue where CollectionView.Scrolled event reports incorrect FirstVisibleItemIndex values on iOS and MacCatalyst after a programmatic ScrollTo() call. The root cause was that the previous implementation relied on IndexPathsForVisibleItems.First(), which includes prefetched cells that are not actually visible on screen.
Changes:
- Replaced the direct use of
IndexPathsForVisibleItems.First()with a newGetIndexPathAtPoint()method that usesIndexPathForItemAtPoint()to determine the actual first visible item - Applied the same logic for determining the center item index
- Added UI tests to verify the fix works correctly
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33614.cs | NUnit test that verifies FirstVisibleItemIndex is correct after programmatic scroll |
| src/Controls/tests/TestCases.HostApp/Issues/Issue33614.cs | Test page demonstrating the issue with a CollectionView, button to trigger scroll, and label showing the index |
| src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs | Updated to use GetIndexPathAtPoint() method for both first and center item indices (active iOS/MacCatalyst handler) |
| src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs | Updated to use GetIndexPathAtPoint() method for both first and center item indices (deprecated iOS handler) |
| App.WaitForElement("ScrollToButton"); | ||
| App.Tap("ScrollToButton"); | ||
| var firstIndexText = App.FindElement("FirstIndexLabel").GetText(); | ||
| Assert.That(firstIndexText, Is.EqualTo("FirstVisibleItemIndex: 15")); |
There was a problem hiding this comment.
The test may have a timing issue. After tapping the button, the test immediately reads the label text without waiting for the scroll animation to complete or for the label to update. This could lead to a race condition where the test reads the old value before the Scrolled event fires and updates the label.
Consider adding a wait or retry mechanism to ensure the label has been updated with the expected value. For example, you could use App.WaitForElement with a lambda that checks for the expected text, or add Task.Delay similar to other tests that wait for scroll animations (see Issue18961.cs line 29 which uses await Task.Delay(1000) after a scroll).
|
|
||
| namespace Maui.Controls.Sample.Issues; | ||
|
|
||
| [Issue(IssueTracker.Github, 33614, "CollectionView Scrolled event reports incorrect FirstVisibleItemIndex after programmatic ScrollTo", PlatformAffected.iOS)] |
There was a problem hiding this comment.
The PlatformAffected attribute should include macOS (MacCatalyst). According to the linked issue #33614, this bug affects both iOS and MacCatalyst. Since MacCatalyst uses the same iOS handlers (Items2/iOS), the fix applies to both platforms. The attribute should be updated to PlatformAffected.iOS | PlatformAffected.macOS.
| [Issue(IssueTracker.Github, 33614, "CollectionView Scrolled event reports incorrect FirstVisibleItemIndex after programmatic ScrollTo", PlatformAffected.iOS)] | |
| [Issue(IssueTracker.Github, 33614, "CollectionView Scrolled event reports incorrect FirstVisibleItemIndex after programmatic ScrollTo", PlatformAffected.iOS | PlatformAffected.macOS)] |
|
/rebase |
492a2bd to
da23222
Compare
🔧 Try-Fix Continuation (Attempts 5-6)Previous comment hit GitHub's 65536 character limit. Continuing with remaining attempts... Fix Attempt 5: Leading-edge Proximity with AdjustedContentInsetModel: gpt-5.1-codex-max Approach: Uses // Calculate visible area top edge using AdjustedContentInset
var visibleTop = collectionView.ContentOffset.Y + collectionView.AdjustedContentInset.Top;
// Find first item whose top edge is at or below visible area
foreach (var indexPath in indexPathsForVisibleItems)
{
var attrs = collectionView.CollectionViewLayout.LayoutAttributesForItem(indexPath);
if (attrs != null && attrs.Frame.Y >= visibleTop - 1) // 1pt tolerance
return indexPath;
}Key Advantages:
Fix Attempt 6: Binary Search + Layout AttributesModel: claude-sonnet-4 Approach: O(log n) binary search through static NSIndexPath GetFirstTrulyVisibleIndexPath(UICollectionView collectionView, List<NSIndexPath> items)
{
var visibleTop = collectionView.ContentOffset.Y + collectionView.ContentInset.Top;
int lo = 0, hi = items.Count - 1;
NSIndexPath result = items[0];
while (lo <= hi)
{
int mid = (lo + hi) / 2;
var attrs = collectionView.CollectionViewLayout.LayoutAttributesForItem(items[mid]);
if (attrs != null && attrs.Frame.Y >= visibleTop)
{
result = items[mid];
hi = mid - 1; // Look for earlier item
}
else
{
lo = mid + 1;
}
}
return result;
}Key Advantages:
🏆 Model Consensus: Best ApproachThree models (opus, gpt-5.2-codex, gemini) evaluated all 7 approaches:
RecommendationThe current PR fix is acceptable for this issue:
For future robustness (grid layouts, gaps, headers), |
PureWeen
left a comment
There was a problem hiding this comment.
Can you evaluate the approaches recommended and see if this influences the approach here?
For example LayoutAttributesForElementsInRect appears to be a favorite!
@PureWeen, I updated the fix to use LayoutAttributesForElementsInRect to find the first visible item index |
|
/rebase |
7b23572 to
858c4b0
Compare
🤖 AI Summary📊 Expand Full Review —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #33719 | Replace IndexPathsForVisibleItems.First() with a layout-attribute scan over the actual visible rect to choose the first visible PENDING (Gate) |
ItemsViewDelegator.cs, ItemsViewDelegator2.cs, Issue33614.cs (HostApp/UI test pair) |
Current PR aligns with prior human feedback favoring LayoutAttributesForElementsInRect. |
cell. |
🚦 Gate — Test Verification
Gate Result PASSED:
Platform: ios
Mode: Full Verification
- Tests FAIL without fix:
- Tests PASS with fix:
Notes
- Verification ran through the repository
verify-tests-fail-without-fixflow forIssue33614. - Decisive result: the UI test failed against the broken baseline and passed once the PR fix files were restored.
- The verification agent reported successful build/deploy/test execution on iOS simulator, confirming the PR's test actually catches the issue it covers.
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | Point-query strategy using IndexPathForItemAtPoint, extra leading-edge probes, and fallback checks. | PASS | 2 files | Valid alternative, but more fallback code than the PR fix. |
| 2 | try-fix (claude-sonnet-4.6) | Walk sorted visible index paths and return the first item whose per-item layout attributes intersect the visible rect. | PASS | 2 files | Simple and early-exit, but depends on visible index ordering. |
| 3 | try-fix (gpt-5.3-codex) | Use UICollectionView.VisibleCells plus leading-edge geometry based on content offset and insets. | PASS | 2 files | Uses rendered cells only, independent from attribute scans. |
| 4 | try-fix (gemini-3-pro-preview) | Sort visible index paths and return the first item whose frame end crosses the viewport threshold using adjusted insets. | PASS | 2 files | Geometry-based threshold strategy; more assumptions than the PR fix. |
| 5 | try-fix (cross-pollination) | Cache first-visible state from WillDisplayCell and DidEndDisplayingCell instead of querying geometry at scroll time. | FAIL | 4 files | Reproduced the bug because lifecycle callbacks lag the final Scrolled event. |
| PR | PR #33719 | Scan layout attributes for elements in the visible rect, filter to cells, and pick the earliest visible one. | PASS (Gate) | 4 files | Original PR fix verified by gate. |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | No | NO NEW IDEAS |
| claude-sonnet-4.6 | 2 | Yes | NEW IDEA: cache first-visible state from cell lifecycle callbacks and use that instead of geometry queries. Tested and failed. |
| gpt-5.3-codex | 2 | No | NO NEW IDEAS |
| gemini-3-pro-preview | 2 | No | NO NEW IDEAS |
Exhausted: Yes
Selected Fix: PR - It is the most robust passing option because it queries current visible geometry directly, avoids ordering and lifecycle timing assumptions, and aligns with the prior human feedback favoring LayoutAttributesForElementsInRect.
📋 Report — Final Recommendation
Final Recommendation: APPROVE
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight COMPLETE | Issue and PR context gathered, files classified, prior review feedback incorporated. | |
| Gate PASSED | iOS full verification confirmed FAIL without fix and PASS with fix for Issue33614. | |
| Try-Fix COMPLETE | 5 attempts total, 4 passing alternatives, 1 failing stateful cross-pollination idea. | |
| Report COMPLETE |
Summary
PR #33719 fixes the reported iOS and MacCatalyst CollectionView bug by replacing the naive use of IndexPathsForVisibleItems.First() with a visible-rect layout-attribute query. The gate verified that the added UI test actually catches the bug on iOS. Independent try-fix exploration found several other geometry-based solutions that also pass, but the PR's approach is the most robust of the passing options.
Root Cause
UIKit can include prefetched cells in IndexPathsForVisibleItems even when those cells are already outside the viewport after a programmatic ScrollTo animation. Using the first entry from that collection therefore reports a stale first visible item index. Any correct fix must derive the answer from the collection view's current visible geometry at the time the Scrolled event is raised.
Fix Quality
The PR fix directly interrogates the visible rect through LayoutAttributesForElementsInRect, filters to actual cells, and chooses the earliest visible item. Compared with the passing alternatives, it has the best tradeoff of correctness and robustness because it does not depend on visible-index ordering, hit-test probe fallbacks, or cell lifecycle timing. The extra stateful cross-pollination idea failed exactly because lifecycle bookkeeping lags the final Scrolled callback, which reinforces that the PR's geometry-at-query-time approach is the right class of solution.
Residual Notes
- The added UI test passed in gate verification, although it still reads the label immediately after tapping the animated ScrollTo trigger; that is worth keeping an eye on if future flakiness appears.
- The HostApp issue page still declares PlatformAffected.iOS even though the issue description and handler fix cover MacCatalyst too; this does not block approval for the iOS review requested here, but it is a follow-up worth considering.
📋 Expand PR Finalization Review
PR #33719 Finalization Review
Title
Current: [iOS] Fix incorrect FirstVisibleItemIndex reported by CollectionView.Scrolled after programmatic scroll
Assessment: Good as-is.
It is specific, searchable, and matches the implementation at a high level.
Description
Assessment: Needs update before merge.
The current body is no longer accurate:
- It says the fix added a
GetIndexPathAtPoint()method, but the actual implementation usesGetFirstVisibleIndexPathUsingLayoutAttributes(...)withLayoutAttributesForElementsInRect(...). - It says Android and Windows were validated, but the code changes are iOS/MacCatalyst-only and the PR does not include evidence for Android/Windows validation.
- The issue and test coverage target both iOS and MacCatalyst, but the write-up does not clearly reflect the final platform scope.
Recommended direction: Keep the existing NOTE block and issue context, but update the Description of Change to describe the final layout-attributes-based approach and narrow the validation/platform wording to what was actually exercised.
A good replacement for the main body would be:
### Issue Details
On iOS and MacCatalyst, `CollectionView.Scrolled` could report an incorrect `FirstVisibleItemIndex` after a programmatic `ScrollTo(..., position: ScrollToPosition.Start, animate: true)`.
### Root Cause
The previous implementation used `IndexPathsForVisibleItems.First()` to determine the first visible item. On iOS/MacCatalyst, that collection can include prefetched cells outside the actual viewport, so the reported first visible index could be earlier than the item shown at the leading edge.
### Description of Change
Updated the iOS CollectionView delegators to compute the first visible item from `LayoutAttributesForElementsInRect(...)` and select the first visible cell that intersects the current viewport. This avoids relying on prefetched cells when raising `Scrolled`.
Applied the fix to both:
- `src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs`
- `src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs`
Added a HostApp repro page and UITest for issue #33614.
### Issues Fixed
Fixes #33614Code Review Findings
Critical issues
1. UITest is race-prone after animated ScrollTo
- File:
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33614.cs - Problem: The test taps
ScrollToButtonand immediately readsFirstIndexLabel. Because the scroll is animated (animate: true), the label can still contain the old value when the assertion runs. - Why it matters: This can produce intermittent failures even if the handler fix is correct.
- Recommendation: Wait for the expected post-scroll state before asserting, similar to other scrolling tests in this repo. For example, wait/retry until
FirstIndexLabelbecomesFirstVisibleItemIndex: 15, or use a deliberate async delay pattern if no better wait primitive is available.
2. Issue page metadata should include MacCatalyst
- File:
src/Controls/tests/TestCases.HostApp/Issues/Issue33614.cs - Problem: The page is marked with
PlatformAffected.iOS, but issue [iOS, Mac] CollectionView Scrolled event reports incorrect FirstVisibleItemIndex after programmatic ScrollTo #33614 is explicitly filed for both iOS and macOS, and this PR updates both shared iOS handlers (Items/andItems2/) used by MacCatalyst. - Why it matters: The test metadata understates the affected platform surface and makes the issue page harder to discover/run for MacCatalyst verification.
- Recommendation: Update the attribute to include macOS/MacCatalyst, e.g.
PlatformAffected.iOS | PlatformAffected.macOS.
Suggestions
- Consider updating the PR description to mention that the final implementation followed the review feedback favoring
LayoutAttributesForElementsInRect(...). - If the author wants to keep the platform prefix broad, the current title is acceptable; the more important fix is making the body accurate.
Looks good
- The functional fix was applied to both iOS handler stacks: legacy
Items/and currentItems2/, which keeps behavior aligned. - Using layout attributes to determine the first visible item is a better match for the reported prefetching problem than relying on
IndexPathsForVisibleItems.First(). - The PR includes a targeted HostApp repro page plus a UITest, which is the right validation shape for this bug.
Merge readiness
Recommendation: Not ready to merge yet.
The handler change itself looks reasonable, but I would address the two open review items above and refresh the PR description before merging.
Additional note: GitHub check metadata for this PR is not currently green from the data I could read (maui-pr shows failures and one run still appears in progress/pending). I did not investigate CI failures here because that is outside this skill's scope.
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33719Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 33719" |
|
@kubaflo, I addressed all the AI review concerns. |
…Scrolled after programmatic scroll (#33719) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details On iOS and MacCatalyst, the CollectionView.Scrolled event reports an incorrect FirstVisibleItemIndex after a programmatic ScrollTo() call. The reported index does not match the actual first visible item displayed on the screen. ### Root Cause The previous implementation used `IndexPathsForVisibleItems.First()` to determine the first visible item. On iOS/MacCatalyst, that collection can include prefetched cells outside the actual viewport, so the reported first visible index could be earlier than the item shown at the leading edge. ### Description of Change Updated the iOS CollectionView delegators to compute the first visible item from `LayoutAttributesForElementsInRect(...)` and select the first visible cell that intersects the current viewport. This avoids relying on prefetched cells when raising `Scrolled`. Applied the fix to both: [ItemsViewDelegator.cs](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) [ItemsViewDelegator2.cs](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) Validated the behavior in the following platforms - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes #33614 ### Output ScreenShot |Before|After| |--|--| | <video src="https://github.com/user-attachments/assets/484a1141-3cfc-470b-9637-39269679832b" >| <video src="https://github.com/user-attachments/assets/efe17504-4a79-4581-aabb-4443ef779580">|
…Scrolled after programmatic scroll (#33719) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details On iOS and MacCatalyst, the CollectionView.Scrolled event reports an incorrect FirstVisibleItemIndex after a programmatic ScrollTo() call. The reported index does not match the actual first visible item displayed on the screen. ### Root Cause The previous implementation used `IndexPathsForVisibleItems.First()` to determine the first visible item. On iOS/MacCatalyst, that collection can include prefetched cells outside the actual viewport, so the reported first visible index could be earlier than the item shown at the leading edge. ### Description of Change Updated the iOS CollectionView delegators to compute the first visible item from `LayoutAttributesForElementsInRect(...)` and select the first visible cell that intersects the current viewport. This avoids relying on prefetched cells when raising `Scrolled`. Applied the fix to both: [ItemsViewDelegator.cs](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) [ItemsViewDelegator2.cs](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) Validated the behavior in the following platforms - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes #33614 ### Output ScreenShot |Before|After| |--|--| | <video src="https://github.com/user-attachments/assets/484a1141-3cfc-470b-9637-39269679832b" >| <video src="https://github.com/user-attachments/assets/efe17504-4a79-4581-aabb-4443ef779580">|
…Scrolled after programmatic scroll (dotnet#33719) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details On iOS and MacCatalyst, the CollectionView.Scrolled event reports an incorrect FirstVisibleItemIndex after a programmatic ScrollTo() call. The reported index does not match the actual first visible item displayed on the screen. ### Root Cause The previous implementation used `IndexPathsForVisibleItems.First()` to determine the first visible item. On iOS/MacCatalyst, that collection can include prefetched cells outside the actual viewport, so the reported first visible index could be earlier than the item shown at the leading edge. ### Description of Change Updated the iOS CollectionView delegators to compute the first visible item from `LayoutAttributesForElementsInRect(...)` and select the first visible cell that intersects the current viewport. This avoids relying on prefetched cells when raising `Scrolled`. Applied the fix to both: [ItemsViewDelegator.cs](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) [ItemsViewDelegator2.cs](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) Validated the behavior in the following platforms - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes dotnet#33614 ### Output ScreenShot |Before|After| |--|--| | <video src="https://github.com/user-attachments/assets/484a1141-3cfc-470b-9637-39269679832b" >| <video src="https://github.com/user-attachments/assets/efe17504-4a79-4581-aabb-4443ef779580">|
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details
On iOS and MacCatalyst, the CollectionView.Scrolled event reports an incorrect FirstVisibleItemIndex after a programmatic ScrollTo() call. The reported index does not match the actual first visible item displayed on the screen.
Root Cause
The previous implementation used
IndexPathsForVisibleItems.First()to determine the first visible item. On iOS/MacCatalyst, that collection can include prefetched cells outside the actual viewport, so the reported first visible index could be earlier than the item shown at the leading edge.Description of Change
Updated the iOS CollectionView delegators to compute the first visible item from
LayoutAttributesForElementsInRect(...)and select the first visible cell that intersects the current viewport. This avoids relying on prefetched cells when raisingScrolled.Applied the fix to both:
ItemsViewDelegator.cs
ItemsViewDelegator2.cs
Validated the behavior in the following platforms
Issues Fixed
Fixes #33614
Output ScreenShot
33614-BeforeFix.mov
33614-AfterFix.mov