[iOS] Fix CollectionView ScrollOffset not resetting when ItemsSource changes - #34488
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34488Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34488" |
There was a problem hiding this comment.
Pull request overview
Fixes iOS CollectionView VerticalOffset not resetting to 0 when ItemsSource changes, by explicitly resetting ContentOffset after ReloadData() in both Items/ and Items2/ iOS view controllers.
Changes:
- Reset
ContentOffsettoCGPoint.EmptyafterReloadData()in bothItemsViewController.csandItemsViewController2.cs, with a guard to skip when already at origin - Updated test page with a
ScrollToEndbutton (usingScrollTo) replacing unreliableDragCoordinates, and removed platform-specific test skips
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs |
Reset ContentOffset after ReloadData in deprecated iOS handler |
src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs |
Reset ContentOffset after ReloadData in current iOS handler |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/XFIssue/Issue7993.cs |
Removed platform skips, replaced DragCoordinates with ScrollToEnd tap |
src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue7993.xaml |
Added ScrollToEnd button, AutomationIds, updated instructions |
src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue7993.xaml.cs |
Added ScrollToEndClicked handler |
kubaflo
left a comment
There was a problem hiding this comment.
Could you please review the AI summary comment?
29d06e5 to
2b52feb
Compare
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
@kubaflo , I have reviewed the AI summary and implemented the suggested fix. |
🤖 AI Summary
📊 Review Session —
|
| Check | Expected | Actual | Result |
|---|---|---|---|
| Tests WITHOUT fix | FAIL | FAIL | ✅ |
| Tests WITH fix | PASS | PASS | ✅ |
✅ Final Verdict
VERIFICATION PASSED ✅
The tests correctly detect the issue:
- ✅ Tests FAIL without the fix (as expected - bug is present)
- ✅ Tests PASS with the fix (as expected - bug is fixed)
Conclusion: The tests properly validate the fix and catch the bug when it's present.
Configuration
Platform: ios
Test Filter: Issue7993
Base Branch: main
Merge Base: a1731cf
Fix Files
src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cssrc/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cssrc/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cssrc/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs
New Files (Not Reverted)
src/Controls/src/Core/Handlers/Items/iOS/IScrollTrackingDelegator.cs
Test Results Details
Test Run 1: WITHOUT Fix
Expected: Tests should FAIL (bug is present)
Actual: Tests FAILED ✅
View full test output (without fix)
Determining projects to restore...
Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 516 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 513 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 17.29 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Foldable/src/Controls.Foldable.csproj (in 17.48 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 17.5 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj (in 17.51 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Maps/src/Controls.Maps.csproj (in 17.51 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj (in 17.5 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 17.51 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/BlazorWebView/src/Maui/Microsoft.AspNetCore.Components.WebView.Maui.csproj (in 17.51 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/maps/src/Maps.csproj (in 17.51 sec).
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-ios26.0/Microsoft.Maui.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Maps.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-ios26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Foldable.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Maps.dll
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-ios/iossimulator-arm64/Controls.TestCases.HostApp.dll
Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
Optimizing assemblies for size. This process might take a while.
Build succeeded.
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
1 Warning(s)
0 Error(s)
Time Elapsed 00:02:06.91
Test Run 2: WITH Fix
Expected: Tests should PASS (bug is fixed)
Actual: Tests PASSED ✅
View full test output (with fix)
Determining projects to restore...
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 355 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 373 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 377 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 410 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 423 ms).
6 of 11 projects are up-to-date for restore.
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-ios26.0/Microsoft.Maui.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Maps.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13941296
Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Maps.dll
Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-ios26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Foldable.dll
Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-ios/iossimulator-arm64/Controls.TestCases.HostApp.dll
Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
Optimizing assemblies for size. This process might take a while.
Build succeeded.
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
1 Warning(s)
0 Error(s)
Time Elapsed 00:00:57.30
Logs
- Full verification log:
/Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/PRState/34488/PRAgent/gate/verify-tests-fail/verification-log.txt - Test output without fix:
/Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/PRState/34488/PRAgent/gate/verify-tests-fail/test-without-fix.log - Test output with fix:
/Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/PRState/34488/PRAgent/gate/verify-tests-fail/test-with-fix.log - UI test logs:
CustomAgentLogsTmp/UITests/
🧪 UI Tests — Category Detection
Detected UI test categories: CollectionView
🔍 Pre-Flight — Context & Validation
Issue: #26366 - [IOS] CollectionView ScrollOffset does not reset when the ItemSource is changed in iOS.
PR: #34488 - [iOS] Fix CollectionView ScrollOffset not resetting when ItemsSource changes
Platforms Affected: iOS, MacCatalyst
Files Changed: 5 implementation, 3 test
Key Findings
- On iOS/MacCatalyst,
UICollectionView.ReloadData()does NOT resetContentOffset. When the ItemsSource changes, the delegator'sPreviousVerticalOffsetretains its stale value, so the nextscrollViewDidScrollcallback reports a delta computed from the old offset rather than zero. - The fix introduces an
IScrollTrackingDelegatorinternal interface withResetScrollTracking()and resets bothPreviousHorizontalOffset/PreviousVerticalOffsetto 0 before forcibly settingContentOffset = CGPoint.EmptyinUpdateItemsSource(). - Both CV1 (
ItemsViewController/ItemsViewDelegator) and CV2 (ItemsViewController2/ItemsViewDelegator2) code paths are addressed symmetrically. - The ordering — call
ResetScrollTracking()BEFORE assigningContentOffset = CGPoint.Empty— is important: UIKit may firescrollViewDidScrollsynchronously during the assignment, and the offset must already be zeroed in the delegator at that moment. - Test refactoring replaces a flaky
DragCoordinatesgesture with a deterministicScrollTobutton tap and adds properWaitForNoElement("VerticalOffset: 0")guard to ensure the scroll actually occurred before verifying the reset. - Previous platform skip (
#if TEST_FAILS_ON_CATALYST && TEST_FAILS_ON_IOS) is removed, enabling the test on all platforms. - One inline review comment (from Copilot PR reviewer) requested the non-zero guard on the label; this was addressed.
Code Review Summary
Verdict: LGTM
Confidence: medium
Errors: 0 | Warnings: 3 | Suggestions: 2
Key code review findings:
⚠️ PlatformAffected.Androidattribute inIssue7993.xaml.csis stale — fix is iOS/macCatalyst-specific, should bePlatformAffected.iOSorPlatformAffected.All⚠️ Fix silently no-ops for subclasses that overrideCreateDelegator()with a type that doesn't implementIScrollTrackingDelegator— safe but undocumented⚠️ CI:maui-pr-uitestsaggregate check is marked failure due to an unrelated Android Border/Button flake; all CollectionView jobs passed ✅- 💡 Consider unconditionally resetting
PreviousH/VOffsetinUpdateItemsSourceregardless of whetherContentOffsetis already at zero - 💡 Cross-namespace dependency:
Items2/iOSnow imports fromItems/iOSnamespace forIScrollTrackingDelegator
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34488 | Add IScrollTrackingDelegator interface; reset PreviousH/VOffset + ContentOffset in UpdateItemsSource() for both CV1 and CV2 |
✅ PASSED (Gate) | 5 impl + 3 test | Original PR |
🔬 Code Review — Deep Analysis
Code Review — PR #34488
Independent Assessment
What this changes: Adds a new internal IScrollTrackingDelegator interface with a single ResetScrollTracking() method. Both ItemsViewDelegator (Items/) and ItemsViewDelegator2 (Items2/) implement it by zeroing PreviousHorizontalOffset/PreviousVerticalOffset. UpdateItemsSource() in both view controllers now resets ContentOffset to CGPoint.Empty when it's non-zero after ReloadData(), and calls ResetScrollTracking() on the delegator before the offset assignment. The test is reworked to use a programmatic ScrollTo button instead of a drag gesture, and the iOS/macCatalyst platform skip is removed.
Inferred motivation: UIKit's UICollectionView.ReloadData() does not reset ContentOffset. After an ItemsSource swap, the collection view renders new items from the top, but ContentOffset stays at the old scroll position. The next scrollViewDidScroll fires with that stale offset, and the MAUI delegator computes delta as currentOffset - PreviousOffset — yielding an incorrect (non-zero) VerticalOffset in the Scrolled event even though the list is visually at the top.
Reconciliation with PR Narrative
Author claims: Setting ContentOffset = CGPoint.Empty after ReloadData, and resetting tracking offsets first so the UIKit-triggered scrollViewDidScroll callback computes delta from zero.
Agreement: The root-cause analysis is correct. UIKit does fire scrollViewDidScroll synchronously when ContentOffset is assigned a new value. The ordering (ResetScrollTracking → ContentOffset = Empty) is critical and is done correctly. The test improvements are sound.
Findings
⚠️ Warning — PlatformAffected.Android attribute is stale
src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue7993.xaml.cs, line 5 (approx):
[Issue(IssueTracker.Github, 7993, "...", PlatformAffected.Android)]The bug and fix are iOS/macCatalyst-specific. The attribute is carried over from the original issue where it was filed against Android (the PR even removed the TEST_FAILS_ON_IOS skip). This should be PlatformAffected.iOS or PlatformAffected.All to accurately reflect where the fix applies.
⚠️ Warning — Fix silently no-ops for overridden delegators
In both UpdateItemsSource() methods:
(Delegator as IScrollTrackingDelegator)?.ResetScrollTracking();Delegator is typed as UICollectionViewDelegateFlowLayout. Since CreateDelegator() is protected virtual, subclasses can supply a delegator that doesn't implement IScrollTrackingDelegator. In that case the tracking reset silently does nothing, and the bug would re-appear. The ?. makes it safe, but a code comment acknowledging this would help future contributors who override CreateDelegator().
⚠️ Warning — CI: maui-pr-uitests overall conclusion is failure
The aggregate maui-pr-uitests and Build Analysis checks are marked failed. However, inspecting the individual jobs shows the failed job is Android UITests Controls (API 30) Border,BoxView,Brush,Button — unrelated to CollectionView or this PR's changes. All CollectionView-specific jobs (iOS CV1, iOS CV, Android CV, macOS CV) passed ✅. This looks like a pre-existing flake, but should be confirmed before merge.
💡 Suggestion — Consider unconditionally resetting Previous offsets in UpdateItemsSource
The guard if (CollectionView.ContentOffset != CGPoint.Empty) skips ResetScrollTracking() when the list is already at the top. If PreviousHorizontalOffset/PreviousVerticalOffset drift out of sync (e.g., from an interrupted animation), they won't be corrected in this case. Since UpdateItemsSource always rebuilds the item source, unconditionally zeroing the Previous offsets (separate from whether ContentOffset needs resetting) would be more defensive. The only concern — a spurious large-negative-delta event — can't happen when ContentOffset is already zero (UIKit won't fire scrollViewDidScroll with no offset change).
💡 Suggestion — Cross-namespace dependency between Items2/ and Items/
ItemsViewController2.cs now adds using Microsoft.Maui.Controls.Handlers.Items; to use IScrollTrackingDelegator. Since Items/ iOS code is deprecated for iOS/macCatalyst, this creates a subtle coupling. An alternative is moving the interface to a shared location (e.g., alongside IItemsViewSource) or into the Items2 namespace. Not blocking, but worth noting as the Items2 handler matures.
Devil's Advocate
On the ordering concern: "Is there evidence that ContentOffset = CGPoint.Empty triggers scrollViewDidScroll synchronously?" — Yes. UIKit's setContentOffset: calls delegate methods synchronously when no animation is requested (which is the case here). The fix would be incorrect if scrollViewDidScroll fired asynchronously, but it does not.
On the guard condition: Could the fix miss cases where ContentOffset is technically non-zero due to floating-point imprecision (e.g., {x=0.0000001, y=0})? The CGPoint equality operator uses exact float comparison, but iOS layout is pixel-snapped so this is unlikely to matter in practice.
On Android coverage: The author didn't validate Android or Windows. However, the fix code (CoreGraphics.CGPoint, ContentOffset) lives under Items/iOS/ and Items2/iOS/, so it only compiles for iOS/macCatalyst — no Android regression risk from the fix code itself. The test change (removing the drag gesture, using ScrollTo) does affect all platforms. The CollectionView Android CI job passed ✅, providing adequate coverage.
Verdict: LGTM
Confidence: medium
Summary: The fix is technically sound — the root cause is correct, the ordering of ResetScrollTracking() before ContentOffset = CGPoint.Empty is the right call, and the test improvements are a genuine quality upgrade over the fragile drag-based approach. The outstanding items are a stale PlatformAffected.Android attribute, the silent-no-op behavior for custom delegators (acceptable by convention), and an unrelated CI failure that should be confirmed pre-merge. No correctness errors were found.
Blast Radius
- Scope:
UpdateItemsSource()runs wheneverItemsSourcechanges. This executes for ALL CollectionView instances on iOS/MacCatalyst that change their item source. - Conditional guard: The reset only fires when
ContentOffset != CGPoint.Empty, so lists that were never scrolled are unaffected. - Delegator interface:
IScrollTrackingDelegatorisinternal, so no public API surface is changed. - CarouselView:
CarouselViewControllerinherits fromItemsViewControllerandCarouselViewController2fromItemsViewController2; both callUpdateItemsSource()via the base, so CarouselView gets the same fix.
Failure-Mode Probes
| Scenario | Result |
|---|---|
| ItemsSource changed when already at top (ContentOffset = 0) | Guard prevents redundant assignment; PreviousOffsets NOT reset — potential drift, addressed in 💡 suggestion |
| Horizontal-scroll CollectionView with ItemsSource change | PreviousHorizontalOffset is also reset — correct |
Delegator replaced via virtual CreateDelegator() override |
as IScrollTrackingDelegator returns null — no crash, but tracking not reset — silent bug regression risk |
| CarouselView ItemsSource change | Inherits the fix via base class — correct |
Animated ContentOffset reset (if someone passes animated:true) |
PR uses direct assignment (no animation) — UIKit fires scrollViewDidScroll synchronously — correct |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | Flag- bool on ViewController; set before ContentOffset = Empty, cleared in delegators Scrolled() which resets PreviousOffsets to 0 | based PASS | 4 files | No interface needed; custom delegators inherit via base Scrolled() |
| 2 | try-fix (claude-sonnet-4.6) | Direct concrete on delegator; cast to concrete type, call unconditionally (no guard) before ContentOffset = Empty; no interface, no cross-namespace import | cast PASS | 4 files | Unconditional reset handles stale-offset edge case; no IScrollTrackingDelegator needed |
| 3 | try-fix (gpt-5.3-codex) | Generation increment ItemsSourceVersion in UpdateItemsSource; delegator resets PreviousOffsets when generation changes in FAIL |
4 files | CollectionViewVerticalOffset timed deferred detection too late | out Scrolled() |
| 4 | try-fix (gpt-5.5) | Event- ViewController raises event; delegator subscribes and resets PreviousOffsets synchronously before ContentOffset = Empty | based PASS | 4 files | Proper .NET event decoupling; unconditional; more ceremony than other approaches |
| PR | PR #34488 | Add interface; reset PreviousH/VOffset + ContentOffset in UpdateItemsSource() for both CV1 and CV2, guarded by ContentOffset != CGPoint.Empty | PASSED (Gate) | 5 impl + 3 test | Original PR; clean interface abstraction |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | Yes | Layout-lifecycle sync via PrepareLayout()/FinalizeCollectionViewUpdates() on too complex, adds concerns to layout layer, not pursued |
| claude-sonnet-4.6 | 2 | No | NO NEW IDEAS |
| gpt-5.3-codex | 2 | Yes | ItemsSource-keyed PreviousOffsets map (memory management not pursued |
| gpt-5.5 | 2 | No | NO NEW IDEAS |
Exhausted: Yes (all 4 models queried; round 2 ideas too complex vs. passing alternatives)
Selected Fix: PRs Interface pattern is clean at call site and extensible. 3/4 try-fix alternatives also passed, confirming the fix strategy is fundamentally sound. Gate PASSED, code review LGTM. PR fix is the best choice. fix
📋 Report — Final Recommendation
✅ Final Recommendation: APPROVE
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issue #26366 / #33500, iOS/MacCatalyst CollectionView scroll offset reset |
| Code Review | LGTM (medium) | 0 errors, 3 warnings (stale PlatformAffected attribute, silent no-op for custom delegators, unrelated CI flake), 2 suggestions |
| Gate | ✅ PASSED | ios — tests FAIL without fix, PASS with fix |
| Try-Fix | ✅ COMPLETE | 4 attempts: 3 passing (flag, concrete-cast, event), 1 failing (generation counter); cross-pollination exhausted |
| Report | ✅ COMPLETE |
Code Review Impact on Try-Fix
The code review identified two specific concerns that directly shaped Try-Fix exploration:
- Silent no-op for custom delegators — Attempt 1 (flag approach) addressed this by putting the reset logic in the base
Scrolled()method, ensuring all delegator subclasses inherit it. Attempt 2 (concrete cast) provided a no-interface alternative. Both passing. - Guard edge case (ContentOffset already 0 but PreviousOffset stale) — Attempts 2 and 4 removed the guard entirely (unconditional reset), confirming the fix works without it. The PR's guarded approach is acceptable in practice since the edge case is unlikely.
The failure-mode probe on deferred detection was confirmed empirically: Attempt 3 (generation counter) timed out because checking state in the scroll callback is too late for the test's synchronous expectations.
Summary
PR #34488 fixes a real iOS/MacCatalyst bug: when CollectionView's ItemsSource changes, UIKit's ReloadData() doesn't reset ContentOffset, leaving stale scroll tracking state in the delegator. The fix correctly resets both ContentOffset and PreviousH/VOffset before UIKit can fire scrollViewDidScroll. The test refactoring is a genuine quality improvement (deterministic ScrollTo button vs. fragile drag gesture). Gate passed on iOS, and 3/4 independent try-fix models also found working fixes, confirming the approach is sound.
Root Cause
UICollectionView.ReloadData() on iOS/MacCatalyst does not reset ContentOffset. After an ItemsSource swap, ItemsViewDelegator.PreviousVerticalOffset retains the stale scroll position. The next scrollViewDidScroll callback computes VerticalDelta = currentOffset - PreviousOffset using this stale value, reporting non-zero VerticalOffset even though the list is visually at the top.
Fix Quality
The fix is technically correct and well-structured:
- The ordering (ResetScrollTracking before ContentOffset = Empty) correctly handles UIKit's synchronous
scrollViewDidScrollcallback - Both CV1 and CV2 code paths are addressed symmetrically
IScrollTrackingDelegatorisinternal— no public API impact- Test improvements replace a flaky drag gesture with a deterministic scroll + assertions
Minor items worth noting (non-blocking):
- The
PlatformAffected.Androidattribute inIssue7993.xaml.csis stale (should beiOSorAll) — cosmetic issue - The unrelated Android
Border,BoxView,Brush,ButtonUI test CI failure should be confirmed as a pre-existing flake before merge - The ContentOffset guard could be removed (unconditional reset is more defensive) but this is a low-risk edge case
…changes (#34488) <!-- 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! ### Issue Details - On iOS, when using CollectionView with the Scrolled event, the VerticalOffset value doesn't reset to 0 when changing the ItemsSource. While the collection view displays the new items correctly, the reported scroll position remains at the previous offset value instead of resetting to the top position. ### Root Cause - On iOS, UICollectionView.ReloadData() does not reset ContentOffset. ContentOffset retains its previous value, meaning any subsequent Scrolled callback—which derives VerticalOffset and delta values directly from ContentOffset—reports the stale, non-zero offset. As a result, MAUI's VerticalOffset never resets to 0 after the ItemsSource changes. ### Description of Change - Added logic in ItemsViewController.cs and ItemsViewController2.cs to reset CollectionView.ContentOffset to zero only when it’s not already at the origin. This ensures the scroll position resets correctly when the ItemsSource changes on iOS/MacCatalyst. - Before resetting the offset, ResetScrollTracking() is invoked via the internal IScrollTrackingDelegator interface so that the scrollViewDidScroll callback computes the delta from zero instead of using a stale offset. **Test and UI improvements:** - Updated the test page (Issue7993.xaml) to add a "ScrollToEnd" button for reliably scrolling the list, and updated label and button identifiers for improved test automation. - Added the ScrollToEndClicked handler in Issue7993.xaml.cs to programmatically scroll to the end of the list, supporting the new test flow. - Removed platform-specific test skips and refactored the test (Issue7993.cs) to use the new "ScrollToEnd" and "NewItemsSource" buttons, improving reliability and making the test applicable to iOS/MacCatalyst. ### Issues Fixed Fixes #26366 Fixes #33500 ### Validated the behaviour in the following platforms - [ ] Windows - [ ] Android - [x] iOS - [x] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/a374859f-25ce-445f-a53f-6354c2d3f201"> | <video src="https://github.com/user-attachments/assets/2bdc2dd4-43be-41ef-810c-e4098cba791f"> |
…changes (#34488) <!-- 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! ### Issue Details - On iOS, when using CollectionView with the Scrolled event, the VerticalOffset value doesn't reset to 0 when changing the ItemsSource. While the collection view displays the new items correctly, the reported scroll position remains at the previous offset value instead of resetting to the top position. ### Root Cause - On iOS, UICollectionView.ReloadData() does not reset ContentOffset. ContentOffset retains its previous value, meaning any subsequent Scrolled callback—which derives VerticalOffset and delta values directly from ContentOffset—reports the stale, non-zero offset. As a result, MAUI's VerticalOffset never resets to 0 after the ItemsSource changes. ### Description of Change - Added logic in ItemsViewController.cs and ItemsViewController2.cs to reset CollectionView.ContentOffset to zero only when it’s not already at the origin. This ensures the scroll position resets correctly when the ItemsSource changes on iOS/MacCatalyst. - Before resetting the offset, ResetScrollTracking() is invoked via the internal IScrollTrackingDelegator interface so that the scrollViewDidScroll callback computes the delta from zero instead of using a stale offset. **Test and UI improvements:** - Updated the test page (Issue7993.xaml) to add a "ScrollToEnd" button for reliably scrolling the list, and updated label and button identifiers for improved test automation. - Added the ScrollToEndClicked handler in Issue7993.xaml.cs to programmatically scroll to the end of the list, supporting the new test flow. - Removed platform-specific test skips and refactored the test (Issue7993.cs) to use the new "ScrollToEnd" and "NewItemsSource" buttons, improving reliability and making the test applicable to iOS/MacCatalyst. ### Issues Fixed Fixes #26366 Fixes #33500 ### Validated the behaviour in the following platforms - [ ] Windows - [ ] Android - [x] iOS - [x] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/a374859f-25ce-445f-a53f-6354c2d3f201"> | <video src="https://github.com/user-attachments/assets/2bdc2dd4-43be-41ef-810c-e4098cba791f"> |
…changes (#34488) <!-- 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! ### Issue Details - On iOS, when using CollectionView with the Scrolled event, the VerticalOffset value doesn't reset to 0 when changing the ItemsSource. While the collection view displays the new items correctly, the reported scroll position remains at the previous offset value instead of resetting to the top position. ### Root Cause - On iOS, UICollectionView.ReloadData() does not reset ContentOffset. ContentOffset retains its previous value, meaning any subsequent Scrolled callback—which derives VerticalOffset and delta values directly from ContentOffset—reports the stale, non-zero offset. As a result, MAUI's VerticalOffset never resets to 0 after the ItemsSource changes. ### Description of Change - Added logic in ItemsViewController.cs and ItemsViewController2.cs to reset CollectionView.ContentOffset to zero only when it’s not already at the origin. This ensures the scroll position resets correctly when the ItemsSource changes on iOS/MacCatalyst. - Before resetting the offset, ResetScrollTracking() is invoked via the internal IScrollTrackingDelegator interface so that the scrollViewDidScroll callback computes the delta from zero instead of using a stale offset. **Test and UI improvements:** - Updated the test page (Issue7993.xaml) to add a "ScrollToEnd" button for reliably scrolling the list, and updated label and button identifiers for improved test automation. - Added the ScrollToEndClicked handler in Issue7993.xaml.cs to programmatically scroll to the end of the list, supporting the new test flow. - Removed platform-specific test skips and refactored the test (Issue7993.cs) to use the new "ScrollToEnd" and "NewItemsSource" buttons, improving reliability and making the test applicable to iOS/MacCatalyst. ### Issues Fixed Fixes #26366 Fixes #33500 ### Validated the behaviour in the following platforms - [ ] Windows - [ ] Android - [x] iOS - [x] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/a374859f-25ce-445f-a53f-6354c2d3f201"> | <video src="https://github.com/user-attachments/assets/2bdc2dd4-43be-41ef-810c-e4098cba791f"> |
…changes (#34488) <!-- 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! ### Issue Details - On iOS, when using CollectionView with the Scrolled event, the VerticalOffset value doesn't reset to 0 when changing the ItemsSource. While the collection view displays the new items correctly, the reported scroll position remains at the previous offset value instead of resetting to the top position. ### Root Cause - On iOS, UICollectionView.ReloadData() does not reset ContentOffset. ContentOffset retains its previous value, meaning any subsequent Scrolled callback—which derives VerticalOffset and delta values directly from ContentOffset—reports the stale, non-zero offset. As a result, MAUI's VerticalOffset never resets to 0 after the ItemsSource changes. ### Description of Change - Added logic in ItemsViewController.cs and ItemsViewController2.cs to reset CollectionView.ContentOffset to zero only when it’s not already at the origin. This ensures the scroll position resets correctly when the ItemsSource changes on iOS/MacCatalyst. - Before resetting the offset, ResetScrollTracking() is invoked via the internal IScrollTrackingDelegator interface so that the scrollViewDidScroll callback computes the delta from zero instead of using a stale offset. **Test and UI improvements:** - Updated the test page (Issue7993.xaml) to add a "ScrollToEnd" button for reliably scrolling the list, and updated label and button identifiers for improved test automation. - Added the ScrollToEndClicked handler in Issue7993.xaml.cs to programmatically scroll to the end of the list, supporting the new test flow. - Removed platform-specific test skips and refactored the test (Issue7993.cs) to use the new "ScrollToEnd" and "NewItemsSource" buttons, improving reliability and making the test applicable to iOS/MacCatalyst. ### Issues Fixed Fixes #26366 Fixes #33500 ### Validated the behaviour in the following platforms - [ ] Windows - [ ] Android - [x] iOS - [x] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/a374859f-25ce-445f-a53f-6354c2d3f201"> | <video src="https://github.com/user-attachments/assets/2bdc2dd4-43be-41ef-810c-e4098cba791f"> |
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
Root Cause
Description of Change
Test and UI improvements:
Issues Fixed
Fixes #26366
Fixes #33500
Validated the behaviour in the following platforms
Output
Before.mov
After.mov