Skip to content

[iOS] Fix CollectionView ScrollOffset not resetting when ItemsSource changes - #34488

Merged
kubaflo merged 4 commits into
dotnet:inflight/currentfrom
SyedAbdulAzeemSF4852:fix-26366
Apr 27, 2026
Merged

[iOS] Fix CollectionView ScrollOffset not resetting when ItemsSource changes#34488
kubaflo merged 4 commits into
dotnet:inflight/currentfrom
SyedAbdulAzeemSF4852:fix-26366

Conversation

@SyedAbdulAzeemSF4852

@SyedAbdulAzeemSF4852 SyedAbdulAzeemSF4852 commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

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, 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
  • iOS
  • Mac

Output

Before After
Before.mov
After.mov
@github-actions

github-actions Bot commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34488

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34488"
@dotnet-policy-service dotnet-policy-service Bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Mar 16, 2026
@sheiksyedm
sheiksyedm marked this pull request as ready for review March 16, 2026 10:44
Copilot AI review requested due to automatic review settings March 16, 2026 10:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ContentOffset to CGPoint.Empty after ReloadData() in both ItemsViewController.cs and ItemsViewController2.cs, with a guard to skip when already at origin
  • Updated test page with a ScrollToEnd button (using ScrollTo) replacing unreliable DragCoordinates, 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
@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 19, 2026

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please review the AI summary comment?

@sheiksyedm

Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).
@SyedAbdulAzeemSF4852

Copy link
Copy Markdown
Contributor Author

Could you please review the AI summary comment?

@kubaflo , I have reviewed the AI summary and implemented the suggested fix.

@dotnet dotnet deleted a comment from MauiBot Apr 26, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 26, 2026
@MauiBot

MauiBot commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI Summary

👋 @SyedAbdulAzeemSF4852 — new AI review results are available. Please review the latest session below.

📊 Review Session824b99e · Modified the fix · 2026-04-26 23:02 UTC
🚦 Gate — Test Before & After Fix

Test Verification Report

Date: 2026-04-26 15:10:52 | Platform: IOS | Status: ✅ PASSED

Summary

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.cs
  • src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs
  • src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs
  • src/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 reset ContentOffset. When the ItemsSource changes, the delegator's PreviousVerticalOffset retains its stale value, so the next scrollViewDidScroll callback reports a delta computed from the old offset rather than zero.
  • The fix introduces an IScrollTrackingDelegator internal interface with ResetScrollTracking() and resets both PreviousHorizontalOffset/PreviousVerticalOffset to 0 before forcibly setting ContentOffset = CGPoint.Empty in UpdateItemsSource().
  • Both CV1 (ItemsViewController/ItemsViewDelegator) and CV2 (ItemsViewController2/ItemsViewDelegator2) code paths are addressed symmetrically.
  • The ordering — call ResetScrollTracking() BEFORE assigning ContentOffset = CGPoint.Empty — is important: UIKit may fire scrollViewDidScroll synchronously during the assignment, and the offset must already be zeroed in the delegator at that moment.
  • Test refactoring replaces a flaky DragCoordinates gesture with a deterministic ScrollTo button tap and adds proper WaitForNoElement("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.Android attribute in Issue7993.xaml.cs is stale — fix is iOS/macCatalyst-specific, should be PlatformAffected.iOS or PlatformAffected.All
  • ⚠️ Fix silently no-ops for subclasses that override CreateDelegator() with a type that doesn't implement IScrollTrackingDelegator — safe but undocumented
  • ⚠️ CI: maui-pr-uitests aggregate check is marked failure due to an unrelated Android Border/Button flake; all CollectionView jobs passed ✅
  • 💡 Consider unconditionally resetting PreviousH/VOffset in UpdateItemsSource regardless of whether ContentOffset is already at zero
  • 💡 Cross-namespace dependency: Items2/iOS now imports from Items/iOS namespace for IScrollTrackingDelegator

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 whenever ItemsSource changes. 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: IScrollTrackingDelegator is internal, so no public API surface is changed.
  • CarouselView: CarouselViewController inherits from ItemsViewController and CarouselViewController2 from ItemsViewController2; both call UpdateItemsSource() 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:

  1. 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.
  2. 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 scrollViewDidScroll callback
  • Both CV1 and CV2 code paths are addressed symmetrically
  • IScrollTrackingDelegator is internal — no public API impact
  • Test improvements replace a flaky drag gesture with a deterministic scroll + assertions

Minor items worth noting (non-blocking):

  • The PlatformAffected.Android attribute in Issue7993.xaml.cs is stale (should be iOS or All) — cosmetic issue
  • The unrelated Android Border,BoxView,Brush,Button UI 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

@MauiBot MauiBot removed the s/agent-changes-requested AI agent recommends changes - found a better alternative or issues label Apr 26, 2026
@MauiBot MauiBot added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR labels Apr 26, 2026
@kubaflo
kubaflo changed the base branch from main to inflight/current April 27, 2026 10:22
@kubaflo
kubaflo merged commit dca94dc into dotnet:inflight/current Apr 27, 2026
163 of 169 checks passed
@github-actions github-actions Bot added this to the .NET 10 SR7 milestone Apr 27, 2026
PureWeen pushed a commit that referenced this pull request Apr 28, 2026
…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">
|
PureWeen pushed a commit that referenced this pull request Apr 29, 2026
…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">
|
github-actions Bot pushed a commit that referenced this pull request May 6, 2026
…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">
|
github-actions Bot pushed a commit that referenced this pull request May 25, 2026
…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">
|
@github-actions github-actions Bot locked and limited conversation to collaborators May 27, 2026
@PureWeen PureWeen modified the milestones: .NET 10 SR7, .NET 10 SR8 Jun 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-controls-collectionview CollectionView, CarouselView, IndicatorView collectionview-cv1 collectionview-cv2 community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/ios s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

8 participants