Skip to content

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

Open
SyedAbdulAzeemSF4852 wants to merge 4 commits intodotnet:mainfrom
SyedAbdulAzeemSF4852:fix-26366
Open

[iOS] Fix CollectionView ScrollOffset not resetting when ItemsSource changes#34488
SyedAbdulAzeemSF4852 wants to merge 4 commits intodotnet:mainfrom
SyedAbdulAzeemSF4852:fix-26366

Conversation

@SyedAbdulAzeemSF4852
Copy link
Copy Markdown
Contributor

@SyedAbdulAzeemSF4852 SyedAbdulAzeemSF4852 commented Mar 16, 2026

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
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 16, 2026

🚀 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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
Copy link
Copy Markdown
Collaborator

MauiBot commented Mar 19, 2026

🤖 AI Summary

📊 Expand Full Review824b99e · Modified the fix
🔍 Pre-Flight — Context & Validation

Issue: #26366 - [IOS] CollectionView ScrollOffset does not reset when the ItemSource is changed in iOS.
Issue: #33500 - Re-enable Issue7993 test on iOS/Catalyst
PR: #34488 - [iOS] Fix CollectionView ScrollOffset not resetting when ItemsSource changes
Platforms Affected: iOS, MacCatalyst
Files Changed: 5 implementation/interface, 3 test

Key Findings

  • On iOS, UICollectionView.ReloadData() does not reset ContentOffset, causing MAUI's Scrolled event to report stale offsets after ItemsSource replacement.
  • This is a re-review: a prior agent review found a simpler alternative (boolean flag in controller) and labeled s/agent-changes-requested + s/agent-fix-win. The author has since revised the PR.
  • The author chose the interface-based approach (IScrollTrackingDelegator) rather than adopting the prior agent's boolean flag suggestion.
  • PR fixes both legacy Items handler (ItemsViewController.cs) and Items2 handler (ItemsViewController2.cs).
  • The new IScrollTrackingDelegator interface lives in the Items namespace but is consumed by Items2 — cross-namespace coupling.
  • Copilot inline review comment (resolved): test was missing assertion that offset was non-zero before NewItemsSource. Author addressed this with App.WaitForNoElement("VerticalOffset: 0").
  • Test was previously completely disabled on iOS/Catalyst via #if TEST_FAILS_ON_CATALYST && TEST_FAILS_ON_IOS. PR removes the skip and rewrites the test using a ScrollToEnd button instead of DragCoordinates.
  • Gate: ✅ PASSED (confirmed by most recent Gate comment, 2026-03-28).

File Classification

  • New interface: src/Controls/src/Core/Handlers/Items/iOS/IScrollTrackingDelegator.cs
  • Implementation: src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs
  • Implementation: src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs
  • Implementation: src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs
  • Implementation: src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs
  • UI HostApp test: src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue7993.xaml
  • UI HostApp test: src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue7993.xaml.cs
  • UI NUnit test: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/XFIssue/Issue7993.cs

Edge Cases From Discussion

  • Does resetting ContentOffset before vs. after ReloadData make a difference? (Order: ReloadData → ResetScrollTracking → ContentOffset = Empty)
  • Will the IScrollTrackingDelegator interface cause issues with carousels or grouped collection views (different delegator types)?
  • Cross-namespace dependency: Items2 references the IScrollTrackingDelegator from Items namespace.
  • What if Delegator is null? The ?. null-safe cast handles this, but should be explicit.

Prior Agent Review Summary

  • Prior agent: s/agent-changes-requested + s/agent-fix-win
  • Prior best candidate: Controller bool _scrollTrackingNeedsReset flag — no new files, no interface, no casts
  • Author's response: Implemented the interface-based approach instead

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34488 New IScrollTrackingDelegator interface; ResetScrollTracking() on delegator before ContentOffset reset ✅ PASSED (Gate) 5 impl + 3 test Current PR after revision

🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (opus-4.6) Controller bool _scrollTrackingNeedsReset flag; delegator checks at top of Scrolled() ✅ PASS 4 files No new types, no interface, no cross-namespace coupling; correctly resets both offset AND delta
2 try-fix (sonnet-4.6) Reset ContentOffset BEFORE ReloadData() — pure order-of-operations, no delegator changes ✅ PASS 2 files Most minimal (+3 lines per controller); BUT VerticalDelta in transition event is stale (not reset)
3 try-fix (gpt-5.3-codex) Direct cast to concrete delegator type to zero fields ❌ FAIL 4 files Test timeout; runtime issues with cast approach
4 try-fix (gpt-5.4, gemini unavailable) Temporarily null CollectionView.Delegate during ContentOffset reset ❌ FAIL 2 files Suppressed zero-offset callback too — label never updated
PR PR #34488 New IScrollTrackingDelegator interface; ResetScrollTracking() on delegator before ContentOffset reset ✅ PASSED (Gate) 5 impl + 3 test (incl. new interface file) Correct for both offset AND delta; more abstract than Candidate 1

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 No "NO NEW IDEAS" — design space well-covered
claude-sonnet-4.6 2 Yes Override SetContentOffset in MauiCollectionView platform view — too invasive, not pursued
gpt-5.3-codex 2 Yes Recreate delegator on ItemsSource change — prior agent run already confirmed this fails
gpt-5.4 2 Yes Same as gpt-5.3-codex (recreate delegator) — not pursued for same reason

Exhausted: Yes — remaining ideas are either too invasive or known failures from prior runs
Selected Fix: Candidate #1 — Controller boolean flag approach. Correctly resets both VerticalOffset AND VerticalDelta. No new files, no interface, no cross-namespace coupling. Simpler than the PR while achieving identical correctness.


📋 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE iOS/MacCatalyst bug; 5 impl files + 3 test files; re-review (prior: s/agent-changes-requested)
Gate ✅ PASSED iOS — tests FAIL without fix, PASS with fix
Try-Fix ✅ COMPLETE 4 attempts; 2 passing (Candidates 1 & 2); best alternative is Candidate 1
Report ✅ COMPLETE

Summary

PR #34488 correctly fixes the iOS CollectionView VerticalOffset stale reporting bug. Gate passed and the test quality is good (the previously-missing App.WaitForNoElement("VerticalOffset: 0") assertion was added, resolving the prior inline review comment). However, Try-Fix found a simpler passing alternative (Candidate 1: controller boolean flag, 4 files, no new abstractions) vs the PR's interface-based approach (5 files + 1 new interface file). This is the second agent review — the prior review also recommended REQUEST CHANGES for the same reason, and the author responded by implementing the interface-based approach rather than the simpler flag approach.

Root Cause

UICollectionView.ReloadData() on iOS does not reset ContentOffset. After ItemsSource replacement, the MAUI Scrolled event delegates (ItemsViewDelegator, ItemsViewDelegator2) retain stale PreviousVerticalOffset/PreviousHorizontalOffset values. The next scrollViewDidScroll callback computes delta from these stale values, causing VerticalOffset to remain non-zero.

Fix Quality

The PR's fix is correct — it properly resets both ContentOffset and the delegator's tracking state before ReloadData, ensuring both VerticalOffset (absolute) and VerticalDelta in the Scrolled event are correct after source replacement.

Design concern: The IScrollTrackingDelegator interface adds abstraction overhead that is not needed for this fix:

  • New file: IScrollTrackingDelegator.cs (single-method interface)
  • Cross-namespace coupling: Items2 imports Microsoft.Maui.Controls.Handlers.Items
  • Null-safe as cast at the call site: (Delegator as IScrollTrackingDelegator)?.ResetScrollTracking()

Best alternative (Candidate 1): Add internal bool _scrollTrackingNeedsReset to both controller classes. Set it to true before ContentOffset = CGPoint.Empty in UpdateItemsSource(). In both delegators' Scrolled(), check the flag via _viewController.TryGetTarget(out var vc), zero the Previous*Offset fields, and clear the flag. This achieves identical behavior with 4 files, no new types, no interface, and no cross-namespace dependency.

Note for maintainers: If the interface contract is preferred for future extensibility or testability, the PR's approach is functionally acceptable. The key previously-reported quality gap (missing non-zero offset assertion) has been addressed. The remaining concern is design preference — the interface vs. flag distinction is modest.


@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
Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

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.

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Mar 28, 2026

🚦 Gate — Test Verification

📊 Expand Full Gate824b99e · Modified the fix

Gate Result: ✅ PASSED

Platform: IOS

Tests Detected

# Type Test Name Filter
1 UITest Issue7993 Issue7993

Verification

Step Expected Actual Result
Without fix FAIL FAIL
With fix PASS PASS

Details

  • Failed: CollectionViewVerticalOffset [17 s]
  • 📋 Error: System.TimeoutException : Timed out waiting for element...

Fix Files Reverted

  • eng/pipelines/ci-copilot.yml
  • 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

Base Branch: main | Merge Base: 720a9d4


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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-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)

Projects

None yet

6 participants