Skip to content

[iOS] Fix CollectionView horizontal scroll when empty inside RefreshView#34382

Merged
kubaflo merged 5 commits intodotnet:inflight/currentfrom
praveenkumarkarunanithi:fix-34165
Mar 14, 2026
Merged

[iOS] Fix CollectionView horizontal scroll when empty inside RefreshView#34382
kubaflo merged 5 commits intodotnet:inflight/currentfrom
praveenkumarkarunanithi:fix-34165

Conversation

@praveenkumarkarunanithi
Copy link
Contributor

Root Cause

When RefreshView initializes on iOS, it attaches the native UIRefreshControl to the CollectionView’s scroll layer. This attachment triggers an internal layout pass. During that layout pass, iOS temporarily reports an incorrect container width to the layout engine, typically double the actual screen width.
UICollectionViewCompositionalLayout, which is used by the CV2 handler, uses this incorrect width to calculate the total content size and caches it. After the animation settles and the correct screen width is restored, the layout engine does not recalculate the content size and continues using the cached double-width value.
As a result, the scroll system believes the content is wider than the screen and enables horizontal scrolling.

Description of Change

The fix was implemented by overriding the CollectionViewContentSize property inside CustomUICollectionViewCompositionalLayout. This property is where the scroll system queries the layout for the total content size.
The override intercepts the content size before it is returned to the scroll system. If the layout is vertical and the reported content width is greater than the actual screen width, the width is clamped to the real screen width before being returned.
By ensuring that a vertical layout never reports a content width larger than the screen, horizontal scrolling is prevented.
The change is minimal, localized to a single file, and safe because a vertical layout can never legitimately have content wider than the screen.

Issues Fixed

Fixes #34165

Tested the behaviour in the following platforms

  • Android
  • Windows
  • iOS
  • Mac

Output Video

Before Issue Fix After Issue Fix
Beforefix.mov
AfterFix.mov

@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 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 -- 34382

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34382"

@dotnet-policy-service dotnet-policy-service bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Mar 9, 2026
@vishnumenon2684 vishnumenon2684 added the community ✨ Community Contribution label Mar 10, 2026
@sheiksyedm sheiksyedm marked this pull request as ready for review March 10, 2026 11:04
Copilot AI review requested due to automatic review settings March 10, 2026 11:04
@sheiksyedm
Copy link
Contributor

/azp run maui-pr-uitests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
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 an iOS (and likely MacCatalyst) CollectionView2 behavior where a vertical CollectionView inside a RefreshView can incorrectly report a too-large content width when empty, enabling unintended horizontal scrolling.

Changes:

  • Clamp UICollectionViewCompositionalLayout.CollectionViewContentSize.Width to the current CollectionView.Bounds.Width for vertical layouts.
  • Add a HostApp reproduction page for issue #34165.
  • Add an Appium UI test asserting an empty CollectionView in a RefreshView does not scroll horizontally.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs Overrides CollectionViewContentSize to clamp oversized reported widths for vertical layouts, preventing horizontal scroll.
src/Controls/tests/TestCases.HostApp/Issues/Issue34165.cs Adds the repro page (RefreshView + empty CollectionView) for issue #34165.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34165.cs Adds an Appium test to validate no horizontal scrolling when empty inside RefreshView.

@kubaflo
Copy link
Contributor

kubaflo commented Mar 14, 2026

🤖 AI Summary

📊 Expand Full Review0aaa62c · updated test as per AI concerns.
🔍 Pre-Flight — Context & Validation

Issue: #34165 - CollectionView is scrolling left/right when the collection is empty and inside a RefreshView
PR: #34382 - [iOS] Fix CollectionView horizontal scroll when empty inside RefreshView
Platforms Affected: iOS (also affects MacCatalyst via same CV2 handler)
Files Changed: 1 implementation, 2 test

Key Findings

  • Root Cause: When RefreshView attaches UIRefreshControl to the CollectionView scroll layer on iOS, it triggers a layout pass during which iOS temporarily reports an incorrect (doubled) container width. UICollectionViewCompositionalLayout (used by the CV2 handler) caches this wrong width. After layout settles, the stale cache causes UIScrollView to enable horizontal scrolling on an otherwise vertical layout.
  • Fix Location: CustomUICollectionViewCompositionalLayout in LayoutFactory2.cs (Items2/iOS — CV2 handler only)
  • Fix Approach: Override CollectionViewContentSize property to clamp the returned width to CollectionView.Bounds.Width whenever scroll direction is Vertical and the reported width exceeds the actual bounds.
  • Validation Note: Issue triage noted the bug did NOT reproduce in CV2 with versions up to 10.0.41 — however the PR author confirmed reproduction in CV2 and provided video evidence. The fix is defensively sound regardless.
  • Copilot review comments were already addressed in the PR:
    • Test now wrapped in #if ANDROID || IOS (scrollRight gesture not supported on Windows/MacCatalyst)
    • Assertion uses .Within(1) tolerance
    • HostApp PlatformAffected updated to include macOS
  • Test type: UI test (TestCases.HostApp + TestCases.Shared.Tests)
  • Test filter: Issue34165 on iOS

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34382 Override CollectionViewContentSize in CustomUICollectionViewCompositionalLayout to clamp width for vertical layouts ⏳ PENDING (Gate) LayoutFactory2.cs iOS/CV2 only; minimal, localized change

🚦 Gate — Test Verification

Gate Result: ✅ PASSED

Platform: iOS
Mode: Full Verification

  • Tests FAIL without fix: ✅
  • Tests PASS with fix: ✅

The test EmptyCollectionViewInsideRefreshViewShouldNotScrollHorizontally in Issue34165.cs correctly catches the bug and validates the PR's fix in LayoutFactory2.cs.


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-sonnet-4.6) Override ShouldInvalidateLayoutForBoundsChange: return true when newBounds.Width != CollectionView.Bounds.Width on vertical layout ✅ PASS LayoutFactory2.cs Proactive: forces re-prep on any width change; could trigger more invalidations than needed
2 try-fix (claude-opus-4.6) Override ShouldInvalidateLayoutForBoundsChange: return true when base.CollectionViewContentSize.Width > newBounds.Width on vertical layout ✅ PASS LayoutFactory2.cs Targeted: reads content size during bounds-change check; potential reentrancy risk
PR PR #34382 Override CollectionViewContentSize to clamp width to bounds on vertical layout ✅ PASSED (Gate) LayoutFactory2.cs Output clamping: minimal, no side effects, safest

Cross-Pollination

Model Round New Ideas? Details
claude-sonnet-4.6 2 Proposed (rejected) TargetContentOffset clamp — pure symptom suppression, disallowed by try-fix guidelines
claude-opus-4.6 2 NO NEW IDEAS Covered all viable strategies

Exhausted: Yes — both models queried; no viable new ideas remain.

Selected Fix: PR's fix — All 3 candidates pass tests. PR fix is the simplest (1 property override, no side effects, no extra UIKit calls during layout callbacks), most localized, and most robustly safe. Alternatives introduce unnecessary invalidation cascades (Attempt 1) or potential reentrancy by querying content size during bounds-change evaluation (Attempt 2).


📋 Report — Final Recommendation

✅ Final Recommendation: APPROVE

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Issue #34165, 1 fix file + 2 test files, Copilot comments already addressed
Gate ✅ PASSED iOS — tests FAIL without fix, PASS with fix
Try-Fix ✅ COMPLETE 2 attempts, 2 passing; PR fix selected as best
Report ✅ COMPLETE

Summary

PR #34382 fixes a bug where an empty CollectionView inside a RefreshView enables unwanted horizontal scrolling on iOS. The fix is minimal, well-targeted, and validated by both Gate and independent Try-Fix exploration. Two alternative approaches were found (both passing), but the PR's fix is the simplest and safest of the three.

Root Cause

When RefreshView attaches UIRefreshControl to the CollectionView's scroll layer on iOS, it triggers a transient layout pass during which iOS temporarily reports double the actual container width. UICollectionViewCompositionalLayout (used by the CV2 handler) caches this incorrect width. After layout settles, the stale cache causes UIScrollView to believe content is wider than the screen, enabling horizontal scrolling.

Fix Quality

The PR's fix overrides CollectionViewContentSize in CustomUICollectionViewCompositionalLayout to clamp the reported width to CollectionView.Bounds.Width when:

  • Scroll direction is Vertical
  • CollectionView is not null
  • Reported width exceeds actual bounds width
  • Bounds width is positive (guards against layout-not-ready state)

Strengths:

  • ✅ Minimal change — single property override in one file
  • ✅ No side effects — pure output clamping, no layout invalidation cascades
  • ✅ Defensive guard — safely handles edge cases (null CollectionView, zero-width bounds)
  • ✅ Well-commented — explains the root cause inline
  • ✅ Tests included — HostApp + NUnit UI test with proper platform guards (#if ANDROID || IOS) and tolerance-based assertion (.Within(1))
  • ✅ Copilot review comments already addressed (platform guard, assertion tolerance, PlatformAffected.macOS)

Alternative approaches explored:

  • Attempt 1 (ShouldInvalidateLayoutForBoundsChange on any width change): ✅ PASS but triggers more invalidations than needed
  • Attempt 2 (ShouldInvalidateLayoutForBoundsChange when content > incoming bounds): ✅ PASS but reads content size during bounds-change evaluation (potential reentrancy risk)

Minor observations (non-blocking):

  • The test wraps in #if ANDROID || IOS — MacCatalyst is excluded because ScrollRight with ScrollStrategy.Gesture is unsupported there. The HostApp page correctly marks PlatformAffected.iOS | PlatformAffected.macOS, so MacCatalyst users can still navigate to the page manually.
  • The fix is in Items2/iOS (CV2 handler). Issue triage noted CV2 didn't reproduce with versions up to 10.0.41, but the PR author confirmed and demonstrated reproduction with video evidence. The defensive clamp is safe regardless — a vertical layout should never legitimately report wider content than the viewport.

Selected Fix: PR's fix

Reason: Simplest, most localized, no cascading side effects. All three candidates pass tests; the PR fix wins on safety and minimalism.


📋 Expand PR Finalization Review

PR #34382 Finalization Review

PR: #34382 — [iOS] Fix CollectionView horizontal scroll when empty inside RefreshView
Author: @praveenkumarkarunanithi
Branch: fix-34165main
Files Changed: 3 (1 fix, 2 tests)
Fixes: #34165


Phase 1: Title & Description

⚠️ Title: Needs Minor Fix (Typo)

Current: [iOS] Fix CollectionView horizontal scroll when empty inside RefreshView
Issue: Double space between "when" and "empty"
Recommended: [iOS] Fix CollectionView horizontal scroll when empty inside RefreshView

The title is otherwise accurate and well-structured.


✅ Description: Good — Only Addition Needed

Quality Assessment:

Indicator Status Notes
Root cause Clear explanation of UICollectionViewCompositionalLayout caching incorrect width during RefreshView init
Description of change Explains the CollectionViewContentSize override and the clamping logic
Accuracy Matches actual diff
Issues Fixed Fixes #34165 present
Platforms tested All 4 platforms checked

Only Addition Needed:

The required NOTE block is missing from the top of the description. Prepend:

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

Action: Fix title typo (double space) + prepend NOTE block. Preserve existing description body — it is well written and accurate.


Phase 2: Code Review

✅ Core Fix — LayoutFactory2.cs

public override CGSize CollectionViewContentSize
{
    get
    {
        var size = base.CollectionViewContentSize;
        if (Configuration.ScrollDirection == UICollectionViewScrollDirection.Vertical
            && CollectionView is not null
            && size.Width > CollectionView.Bounds.Width
            && CollectionView.Bounds.Width > 0)
        {
            return new CGSize(CollectionView.Bounds.Width, size.Height);
        }
        return size;
    }
}

Observations:

  • ✅ Minimal, localized change in the right place (CustomUICollectionViewCompositionalLayout)
  • ✅ All necessary guards are present: null check, direction check, bounds > 0 (prevents clamping to zero during initialization)
  • ✅ Only affects vertical layouts — horizontal CollectionViews are unaffected
  • ✅ Comment references the issue number (CollectionView is scrolling left/right when the collection is empty and inside a RefreshView #34165) for traceability
  • ✅ Correctly applies only to the CV2 handler path (Items2/iOS/)

🟡 Suggestions

1. Test uses hardcoded string literals instead of host app constants

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34165.cs

The host app defines constants for AutomationIds:

// TestCases.HostApp/Issues/Issue34165.cs
public const string CollectionViewId = "CollectionView";
public const string EmptyViewLabelId = "EmptyViewLabel";
public const string RefreshViewId = "RefreshView";

But the test uses string literals:

App.WaitForElement("CollectionView");
var rectBefore = App.WaitForElement("EmptyViewLabel").GetRect();

While the strings currently match, using the constants would make refactoring safer. This is a minor style issue — not blocking.

2. Test scope includes Android despite the fix being iOS-only

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34165.cs

#if ANDROID || IOS // ScrollRight with ScrollStrategy.Gesture is not supported on Windows and MacCatalyst

The bug is iOS-specific (the fix lives in Items2/iOS/LayoutFactory2.cs). Including Android in the test isn't harmful — Android uses a different handler and doesn't exhibit this bug — but it's mildly misleading given the host app is tagged PlatformAffected.iOS | PlatformAffected.macOS. The comment explains the rationale (gesture scroll not supported on Windows/MacCatalyst), so this is acceptable as-is.

3. Potential CV1 vs CV2 scope gap

The original issue (#34165) carries the label collectionview-cv1, but the fix is applied to Items2/iOS/LayoutFactory2.cs (CV2 handler). If CV1 (Items/iOS/) also uses UICollectionViewCompositionalLayout, the same bug may exist there. This should be verified — if CV1 has the same issue, a parallel fix may be needed there.


✅ Looks Good

  • ✅ Fix is minimal and surgical — 19 lines in a single file
  • ✅ Well-guarded against edge cases (null CollectionView, zero bounds width, horizontal layouts)
  • ✅ Test verifies the exact regression: X position of EmptyViewLabel must not change after a horizontal swipe gesture
  • ✅ Test uses Within(1) tolerance for pixel-level comparison — good practice
  • ✅ Host app correctly sets up the reproduction scenario (empty CollectionView inside RefreshView with EmptyView)
  • ✅ Host app handles the Refreshing event (stops spinner) so the RefreshView doesn't get stuck

Summary

Check Status
Title accurate ⚠️ Typo (double space)
NOTE block present ❌ Missing — must be added
Description accurate
Root cause documented
Fix approach documented
Issues Fixed linked
Fix code quality
Tests added
Critical issues None

Verdict: Nearly merge-ready. Two mechanical changes needed before merge:

  1. Fix the double space in the title (when emptywhen empty)
  2. Prepend the required NOTE block to the PR description

The code change itself is clean, well-reasoned, and correctly addresses the root cause.

@kubaflo kubaflo 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 s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 14, 2026
@kubaflo kubaflo changed the base branch from main to inflight/current March 14, 2026 13:22
@kubaflo kubaflo merged commit d2fbddc into dotnet:inflight/current Mar 14, 2026
30 of 31 checks passed
kubaflo pushed a commit that referenced this pull request Mar 16, 2026
<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Description of Change

<!-- Enter description of the fix in this section -->

The fix in PR #34382 , currently in the inflight/current branch, causes
conflicts in ItemsFactory2.cs and build failed. Issues
[34165](#34165) and
[17799](#17799) were already
resolved by PR #31215 , which is also included in the inflight/current
branch. Therefore, removing the PR #34382 changes from inflight/current
to resolve the conflicts in ItemsFactory2.cs.
PureWeen pushed a commit that referenced this pull request Mar 19, 2026
…iew (#34382)

### Root Cause
When `RefreshView` initializes on iOS, it attaches the native
`UIRefreshControl` to the CollectionView’s scroll layer. This attachment
triggers an internal layout pass. During that layout pass, iOS
temporarily reports an incorrect container width to the layout engine,
typically double the actual screen width.
`UICollectionViewCompositionalLayout`, which is used by the CV2 handler,
uses this incorrect width to calculate the total content size and caches
it. After the animation settles and the correct screen width is
restored, the layout engine does not recalculate the content size and
continues using the cached double-width value.
As a result, the scroll system believes the content is wider than the
screen and enables horizontal scrolling.
 
### Description of Change
The fix was implemented by overriding the `CollectionViewContentSize`
property inside `CustomUICollectionViewCompositionalLayout`. This
property is where the scroll system queries the layout for the total
content size.
The override intercepts the content size before it is returned to the
scroll system. If the layout is vertical and the reported content width
is greater than the actual screen width, the width is clamped to the
real screen width before being returned.
By ensuring that a vertical layout never reports a content width larger
than the screen, horizontal scrolling is prevented.
The change is minimal, localized to a single file, and safe because a
vertical layout can never legitimately have content wider than the
screen.

### Issues Fixed
Fixes #34165 
 
Tested the behaviour in the following platforms
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

### Output Video
Before Issue Fix | After Issue Fix |
|----------|----------|
|<video width="40" height="60" alt="Before Fix"
src="https://github.com/user-attachments/assets/0acc28a6-a526-4799-8de7-99c4b0dbf4fe">|<video
width="50" height="40" alt="After Fix"
src="https://github.com/user-attachments/assets/61100874-a442-4a50-96ee-0539a2544ac3">|
PureWeen pushed a commit that referenced this pull request Mar 19, 2026
<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Description of Change

<!-- Enter description of the fix in this section -->

The fix in PR #34382 , currently in the inflight/current branch, causes
conflicts in ItemsFactory2.cs and build failed. Issues
[34165](#34165) and
[17799](#17799) were already
resolved by PR #31215 , which is also included in the inflight/current
branch. Therefore, removing the PR #34382 changes from inflight/current
to resolve the conflicts in ItemsFactory2.cs.
PureWeen pushed a commit that referenced this pull request Mar 24, 2026
…iew (#34382)

### Root Cause
When `RefreshView` initializes on iOS, it attaches the native
`UIRefreshControl` to the CollectionView’s scroll layer. This attachment
triggers an internal layout pass. During that layout pass, iOS
temporarily reports an incorrect container width to the layout engine,
typically double the actual screen width.
`UICollectionViewCompositionalLayout`, which is used by the CV2 handler,
uses this incorrect width to calculate the total content size and caches
it. After the animation settles and the correct screen width is
restored, the layout engine does not recalculate the content size and
continues using the cached double-width value.
As a result, the scroll system believes the content is wider than the
screen and enables horizontal scrolling.
 
### Description of Change
The fix was implemented by overriding the `CollectionViewContentSize`
property inside `CustomUICollectionViewCompositionalLayout`. This
property is where the scroll system queries the layout for the total
content size.
The override intercepts the content size before it is returned to the
scroll system. If the layout is vertical and the reported content width
is greater than the actual screen width, the width is clamped to the
real screen width before being returned.
By ensuring that a vertical layout never reports a content width larger
than the screen, horizontal scrolling is prevented.
The change is minimal, localized to a single file, and safe because a
vertical layout can never legitimately have content wider than the
screen.

### Issues Fixed
Fixes #34165 
 
Tested the behaviour in the following platforms
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

### Output Video
Before Issue Fix | After Issue Fix |
|----------|----------|
|<video width="40" height="60" alt="Before Fix"
src="https://github.com/user-attachments/assets/0acc28a6-a526-4799-8de7-99c4b0dbf4fe">|<video
width="50" height="40" alt="After Fix"
src="https://github.com/user-attachments/assets/61100874-a442-4a50-96ee-0539a2544ac3">|
PureWeen pushed a commit that referenced this pull request Mar 24, 2026
<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Description of Change

<!-- Enter description of the fix in this section -->

The fix in PR #34382 , currently in the inflight/current branch, causes
conflicts in ItemsFactory2.cs and build failed. Issues
[34165](#34165) and
[17799](#17799) were already
resolved by PR #31215 , which is also included in the inflight/current
branch. Therefore, removing the PR #34382 changes from inflight/current
to resolve the conflicts in ItemsFactory2.cs.
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-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)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CollectionView is scrolling left/right when the collection is empty and inside a RefreshView

6 participants