Skip to content

[iOS] Fix for CollectionView.Measure() returning incorrect height when called before the view is mounted#34331

Merged
kubaflo merged 3 commits intodotnet:inflight/currentfrom
BagavathiPerumal:fix-32983
Mar 14, 2026
Merged

[iOS] Fix for CollectionView.Measure() returning incorrect height when called before the view is mounted#34331
kubaflo merged 3 commits intodotnet:inflight/currentfrom
BagavathiPerumal:fix-32983

Conversation

@BagavathiPerumal
Copy link
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!

Root Cause:

The issue occurs because CollectionView (Items2 / CollectionViewHandler2) depends on the native UIKit layout pass to compute its content size. When Measure() is called before the view is mounted, the native collection view has not performed layout yet, so ContentSize remains {0,0}. MAUI then falls back to base.GetDesiredSize(), which returns the full constraint height, producing an incorrect measured value.

Fix Description:

The fix involves performing a temporary in-place layout pass during pre-mount measurement. If Window == null, the CollectionView frame is set directly to the given constraints (clamping ∞ to 10000 to match UIKit’s UILayoutFittingExpandedSize). Then SetNeedsLayout() and LayoutIfNeeded() are called so UIKit computes the real content size. The original frame is always restored in a finally block. No AddSubview, RemoveFromSuperview, or ReloadData() is used. This allows Measure() to return the correct height even before the control is mounted.

Reason for Mac behavior:

On macOS via MacCatalyst, UISheetPresentationController always presents sheets as full-height modal panels. Custom detents (custom heights) are ignored by the platform.

Issues Fixed

Fixes #32983

Tested the behaviour in the following platforms

  • iOS
  • Mac
  • Android
  • Windows

Output Screenshot

Before Issue Fix After Issue Fix
32983-BeforeFix.mov
32983-AfterFix.mov

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 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 -- 34331

Or

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

@dotnet-policy-service dotnet-policy-service bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Mar 4, 2026
@vishnumenon2684 vishnumenon2684 added the community ✨ Community Contribution label Mar 4, 2026
@sheiksyedm sheiksyedm marked this pull request as ready for review March 11, 2026 06:23
Copilot AI review requested due to automatic review settings March 11, 2026 06:23
@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 iOS CollectionView (Items2 handler) measuring to an incorrect height when Measure() is invoked before the native view is mounted, by forcing a UIKit layout pass so ContentSize is populated.

Changes:

  • Force a temporary LayoutIfNeeded() pass for pre-mount measurement when ContentSize is still zero.
  • Add a HostApp reproduction page and an iOS UI test verifying the correct bottom-sheet detent height.
  • Add an iOS snapshot baseline for the new UI test.

Reviewed changes

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

File Description
src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs Forces a pre-mount layout pass to compute ContentSize before falling back to base measurement.
src/Controls/tests/TestCases.HostApp/Issues/Issue32983.cs Adds a reproduction page that measures unmounted content and presents it in a sheet detent.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32983.cs Adds an iOS-only UI test that opens the sheet and validates via screenshot.
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/BottomSheetDetentHeightIsCorrectWhenCollectionViewIsMeasuredBeforeMount.png Adds the snapshot baseline for the new screenshot test.

@kubaflo
Copy link
Contributor

kubaflo commented Mar 14, 2026

🤖 AI Summary

📊 Expand Full Reviewbca48f1 · fix-32983-Made the changes to guarding against NaN/negative values.
🔍 Pre-Flight — Context & Validation

Issue: #32983 - CollectionView messes up Measure operation on Views
PR: #34331 - [iOS] Fix for CollectionView.Measure() returning incorrect height when called before the view is mounted
Platforms Affected: iOS (primary); MacCatalyst affected but UISheetPresentationController ignores custom detents there
Files Changed: 1 implementation, 2 test (+ 2 snapshot PNGs)

Key Findings

  • Regression since .NET 10 / preview1: The new CollectionView handler (CV2, Items2) uses UICollectionViewCompositionalLayout. Before mount, no layout pass has run, so ContentSize is {0,0}.
  • Old behavior (.NET 9 / CV1): The original handler measured differently and returned correct height even before mount.
  • Root cause: EnsureContentSizeForScrollDirection() detected contentSize=0 and fell back to base.GetDesiredSize() (returns full constraint height) — wrong when measuring before mount.
  • Fix approach: When collectionView.Window == null (pre-mount), temporarily set frame to clamped constraints, call SetNeedsLayout() + LayoutIfNeeded(), read ContentSize, then restore original frame. This forces UIKit to compute real layout without attaching the view to the hierarchy.
  • Copilot review already applied: A prior copilot review suggested adding a ClampConstraint helper to guard against infinity/NaN before constructing CGRect. The author accepted and implemented this suggestion.
  • Remaining reviewer concern: VerifyScreenshot() without retryTimeout may be flaky due to sheet animation. Author disagreed, claiming WaitForElement("BottomSheetCollectionView") is sufficient synchronization.
  • Test structure: Test is iOS-only (#if IOS), uses UISheetPresentationController with custom detent to measure bottom sheet content before presentation. Uses VerifyScreenshot() for visual validation.
  • No prior agent review detected.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34331 Force layout pass pre-mount via SetNeedsLayout/LayoutIfNeeded with clamped frame ⏳ PENDING (Gate) ItemsViewHandler2.iOS.cs Accepted Copilot suggestion for ClampConstraint

🚦 Gate — Test Verification

Gate Result: ✅ PASSED

Platform: iOS
Mode: Full Verification

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

Details: iPhone 11 Pro simulator. Test BottomSheetDetentHeightIsCorrectWhenCollectionViewIsMeasuredBeforeMount correctly catches the regression (fails without fix, passes with fix).


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34331 Pre-mount layout pass: clamp constraints, set Frame, SetNeedsLayout+LayoutIfNeeded, restore frame ✅ PASSED (Gate) ItemsViewHandler2.iOS.cs ClampConstraint helper accepted from Copilot review
1 try-fix (Sonnet) Same as PR but use Bounds instead of Frame for semantically precise layout space communication ✅ PASS ItemsViewHandler2.iOS.cs Functionally identical to PR; Bounds is more correct (no position change in parent's coordinate space)
2 try-fix (Opus) Bounds + explicit InvalidateLayout() + PrepareLayout() before LayoutIfNeeded() ✅ PASS ItemsViewHandler2.iOS.cs Extra invalidation steps not required; LayoutIfNeeded alone is sufficient per Attempt 1
3 try-fix (Sonnet) Temporarily attach to hidden UIWindow (AddSubview/RemoveFromSuperview) ❌ FAIL ItemsViewHandler2.iOS.cs 6.15% visual diff — UIWindow changes CompositionalLayout behavior; confirms PR's decision to avoid hierarchy manipulation

Cross-Pollination

Model Round New Ideas? Details
Sonnet 2 Yes Temp UIWindow approach (→ Attempt 3)
Opus 2 Yes Bypass UIKit, compute from MAUI measurement system (too complex/fragile — not attempted)
Sonnet 3 No NO NEW IDEAS — frame space exhausted
Opus 3 No NO NEW IDEAS — all lightweight alternatives covered

Exhausted: Yes
Selected Fix: PR's fix — Frame mutation + ClampConstraint + SetNeedsLayout/LayoutIfNeeded + restore in finally

Reason: The PR fix and Attempt 1 are functionally equivalent (Frame vs Bounds makes no measurable difference pre-mount since no superview exists). The PR fix has already been properly reviewed (including an accepted Copilot suggestion for ClampConstraint). Attempt 1's Bounds approach is a minor semantic improvement but does not justify changing the PR. Attempt 3 empirically confirmed that the PR authors were correct to avoid AddSubview/RemoveFromSuperview.


📋 Report — Final Recommendation

✅ Final Recommendation: APPROVE

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE iOS regression in CV2 handler (Items2); pre-mount Measure() returns wrong height
Gate ✅ PASSED iOS — tests FAIL without fix, PASS with fix
Try-Fix ✅ COMPLETE 3 attempts: 2 passing, 1 failing; PR's fix is best candidate
Report ✅ COMPLETE

Summary

PR #34331 fixes a regression introduced in .NET 10's new CollectionView handler (CV2, ItemsViewHandler2.iOS.cs). When CollectionView.Measure() is called before the view is added to the window (a common pattern for bottom sheet sizing), UICollectionViewCompositionalLayout has not run a layout pass, so ContentSize is {0,0}. The old fallback returned base.GetDesiredSize() — the full constraint height — which is wrong.

The fix is well-targeted, correctly gated on collectionView.Window == null, and has been iteratively improved (a Copilot code review suggestion for ClampConstraint was accepted). Gate verification confirmed the test correctly catches the regression.

Root Cause

UICollectionViewCompositionalLayout only computes collectionViewContentSize after a layout pass, which requires a real bounds/frame to work with. Before mounting (Window == null), no layout pass has occurred. The code in EnsureContentSizeForScrollDirection detected contentSize == 0 and fell back to base.GetDesiredSize() which returns the full constraint height — the incorrect direction (constraint ≠ content size).

Fix Quality

Strengths:

  • ✅ Correctly scoped: only activates when Window == null AND contentSize == 0
  • ✅ Safe frame restore via try/finally — original frame always restored
  • ✅ Robust infinity/NaN guard via ClampConstraint (accepted from Copilot review suggestion)
  • ✅ No view-hierarchy manipulation (no AddSubview/RemoveFromSuperview) — empirically validated as the correct choice (Attempt 3's UIWindow approach caused a 6.15% layout difference)
  • ✅ Falls through to original fallback if forced layout still gives zero (double-safety)
  • ✅ Tests catch the bug and verify the fix visually via snapshot

Minor concern (non-blocking):

  • Frame vs Bounds: Attempt 1 showed that using Bounds instead of Frame is semantically more precise (Bounds sets the view's own coordinate space; Frame sets position+size in parent's space). Since there is no superview when Window == null, both are functionally identical, so this is cosmetic only and does not warrant blocking the PR.
  • The VerifyScreenshot() call without retryTimeout could be flaky if the sheet animation is slow. The author's argument (WaitForElement("BottomSheetCollectionView") as synchronization) is reasonable but VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2)) would be more robust per project guidelines.

Try-Fix conclusion: The PR's fix is the best available approach. Alternative passing approaches (Attempt 1: Bounds, Attempt 2: +InvalidateLayout+PrepareLayout) add complexity without benefit. The failing approach (Attempt 3: UIWindow) confirmed the PR authors were correct to avoid hierarchy manipulation.

Selected Fix

PR #34331 — Frame mutation + ClampConstraint + SetNeedsLayout/LayoutIfNeeded + finally restore

Result: ✅ APPROVE


📋 Expand PR Finalization Review

PR #34331 Finalization Review

PR: #34331
Title: [iOS] Fix for CollectionView.Measure() returning incorrect height when called before the view is mounted
Author: BagavathiPerumal (community ✨, partner/syncfusion)
Fixes: #32983
Files Changed: 5 (+223, -2)


Phase 1: Title & Description

🟡 Title: Minor Improvement Suggested

Current:

[iOS] Fix for CollectionView.Measure() returning incorrect height when called before the view is mounted

Assessment: Good structure with [iOS] prefix and clear description of the fix. "Fix for" is wordy noise; removing it produces a tighter commit message. Slightly long for a headline.

Recommended:

[iOS] CollectionView: Fix Measure() returning incorrect height before view is mounted

✅ Description: Good Quality — Minor Additions Needed

Quality assessment:

Indicator Status Notes
NOTE block ✅ Present Correctly at the top
Root cause ✅ Clear UICollectionViewCompositionalLayout hasn't run a layout pass pre-mount
Fix description ✅ Clear Forced layout pass with frame restore in finally block
Issues Fixed ✅ Present Fixes https://github.com/dotnet/maui/issues/32983
Platforms Tested ✅ Present iOS ✅ Mac ✅ Android ❌ Windows ❌
Mac behavior note ✅ Present Explains why MacCatalyst shows full-height sheets
Screenshot section ❌ Empty Both Before/After image slots are empty
Items2-specific scope ❌ Missing Fix only applies to CollectionViewHandler2 (Items2/), not legacy handler
"What NOT to do" ❌ Missing No record of failed alternatives

Overall verdict: The existing description is well-structured and technically sound. Keep it with additions below.

Additions needed:

  1. Add note about Items2-specific scope — The fix is in ItemsViewHandler2.iOS.cs (the new Items2/CollectionViewHandler2 handler introduced in .NET 10). The legacy Items/ handler is unaffected. This context is critical for future agents.

  2. Fill in or remove Screenshots section — Both image slots are empty. Either add screenshots or remove the placeholder.

  3. Add "What NOT to Do" section — Document that AddSubview, RemoveFromSuperview, and ReloadData() were explicitly avoided, as they would trigger side effects.

Recommended additions to description:

### Scope

This fix applies only to **CollectionViewHandler2** (`Items2/`), the new iOS/MacCatalyst handler introduced in .NET 10. The legacy `Items/` handler (used in .NET 9) is unaffected. The regression was introduced specifically because CollectionViewHandler2 relies on `UICollectionViewCompositionalLayout`, which defers layout until after view mounting.

### What NOT to Do (for future agents)

-**Don't use AddSubview/RemoveFromSuperview** — Temporarily attaching the view to a window causes side effects (hit-testing, view hierarchy changes, visibility notifications)
-**Don't call ReloadData()** — This would trigger data source callbacks and potentially cause data inconsistencies
-**Don't compare window-level SafeAreaInsets** — Not applicable here, but: the fix correctly uses `collectionView.Window == null` (per-view state), not any window-level property

Phase 2: Code Review

🔴 Critical Issues

None identified.


🟡 Suggestions

1. VerifyScreenshot() without retryTimeout — potential flakiness

  • File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32983.cs
  • Lines: 20–24
  • Problem: The test taps a button that triggers an animated UISheetPresentationController presentation. The sheet animation takes time. VerifyScreenshot() is called immediately after App.WaitForElement("BottomSheetCollectionView") — the element being present doesn't mean the animation is complete. This can cause screenshot mismatches on slower devices.
  • Recommendation:
    App.WaitForElement("BottomSheetCollectionView");
    VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));

2. _measuredHeightLabel is never updated in HostApp

  • File: src/Controls/tests/TestCases.HostApp/Issues/Issue32983.cs
  • Lines: 18–22, OnShowBottomSheetClicked
  • Problem: _measuredHeightLabel with AutomationId = "MeasuredHeight" has Text = "Pending" and is never updated. The Measure() result (minimumSize) is computed inside OnShowBottomSheetClicked but never displayed. This makes the label dead code, and the AutomationId is unused in the test.
  • Options:
    • Update the label: _measuredHeightLabel.Text = $"{minimumSize.Height:F2}"; after calling Measure()
    • Or remove the label/AutomationId entirely if not used in tests

3. Double-cast overflow guard in ClampConstraint is correct but slightly redundant

  • File: src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs
  • Lines: 218–230 (approx)
  • Problem: The local function casts doublenfloat, then casts back to double to check for overflow/NaN. The comment says "Guard against overflow to infinity/NaN or negative after casting". On platforms where nfloat is float (32-bit), this guard is meaningful. On 64-bit it is effectively a no-op. The code is correct and safe; this is an informational note only.
  • Recommendation: No change required; the defensive check is appropriate for cross-architecture compatibility.

✅ Looks Good

  • Core fix approach is sound: Forcing a layout pass via SetNeedsLayout() + LayoutIfNeeded() with a try/finally frame restore is the correct minimal approach. No view hierarchy manipulation.
  • collectionView.Window == null guard is correct: This reliably detects pre-mount state and gates the expensive forced layout to only when necessary. Mounted views benefit from the normal layout pass.
  • Fallback path preserved: If the forced layout still produces zero content size, the original fallback (base.GetDesiredSize()) is still used — correct defensive behavior.
  • UIView.UILayoutFittingExpandedSize as fallback for ClampConstraint: This matches UIKit's own conventions for "largest possible size" (10000×10000).
  • try/finally frame restore: Guarantees previousFrame is always restored even if LayoutIfNeeded() throws.
  • Test scope (#if IOS): Correctly scoped since UISheetPresentationController custom detents are iOS-only and MacCatalyst always presents full-height.
  • [Category(UITestCategories.CollectionView)]: Correct category for CollectionView tests.
  • Two snapshot folders (ios/ and ios-26/): Correct pattern for iOS-26 Liquid Glass visual differences.
  • PlatformAffected.iOS in [Issue] attribute: Correct — the bug and fix are iOS-specific.
  • #if IOS || MACCATALYST in HostApp: Correct platform guard for UIKit APIs.

Summary

Aspect Status Action
Title 🟡 Minor improvement Remove "Fix for" prefix, tighten phrasing
Description 🟡 Good, minor additions Add Items2 scope note, fill/remove screenshots, add "What NOT to Do"
Core fix logic ✅ Sound No changes needed
Test flakiness risk 🟡 Low-moderate Add retryTimeout to VerifyScreenshot()
Dead UI label 🟡 Minor Update _measuredHeightLabel or remove it
Breaking changes ✅ None Internal Items2 handler change only
Platform coverage ✅ Appropriate iOS-only fix, iOS+Mac tested

@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 16:14
@kubaflo kubaflo merged commit 9d1189c into dotnet:inflight/current Mar 14, 2026
24 of 33 checks passed
PureWeen pushed a commit that referenced this pull request Mar 19, 2026
…n called before the view is mounted (#34331)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Root Cause:

The issue occurs because CollectionView (Items2 /
CollectionViewHandler2) depends on the native UIKit layout pass to
compute its content size. When Measure() is called before the view is
mounted, the native collection view has not performed layout yet, so
ContentSize remains {0,0}. MAUI then falls back to
base.GetDesiredSize(), which returns the full constraint height,
producing an incorrect measured value.

### Fix Description:

The fix involves performing a temporary in-place layout pass during
pre-mount measurement. If Window == null, the CollectionView frame is
set directly to the given constraints (clamping ∞ to 10000 to match
UIKit’s UILayoutFittingExpandedSize). Then SetNeedsLayout() and
LayoutIfNeeded() are called so UIKit computes the real content size. The
original frame is always restored in a finally block. No AddSubview,
RemoveFromSuperview, or ReloadData() is used. This allows Measure() to
return the correct height even before the control is mounted.
 
**Reason for Mac behavior:**
 
On macOS via MacCatalyst, UISheetPresentationController always presents
sheets as full-height modal panels. Custom detents (custom heights) are
ignored by the platform.

### Issues Fixed
Fixes #32983

### Tested the behaviour in the following platforms
- [x] iOS
- [x] Mac
- [ ] Android
- [ ] Windows

### Output Screenshot
Before Issue Fix | After Issue Fix |
|----------|----------|
|<video width="100" height="100" alt="Before Fix"
src="https://github.com/user-attachments/assets/8f31b5fe-6482-490c-b6f4-c77376257042">|<video
width="100" height="100" alt="After Fix"
src="https://github.com/user-attachments/assets/d0b2e698-8843-4d9b-a773-0bd3db427b7c">|
PureWeen pushed a commit that referenced this pull request Mar 24, 2026
…n called before the view is mounted (#34331)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Root Cause:

The issue occurs because CollectionView (Items2 /
CollectionViewHandler2) depends on the native UIKit layout pass to
compute its content size. When Measure() is called before the view is
mounted, the native collection view has not performed layout yet, so
ContentSize remains {0,0}. MAUI then falls back to
base.GetDesiredSize(), which returns the full constraint height,
producing an incorrect measured value.

### Fix Description:

The fix involves performing a temporary in-place layout pass during
pre-mount measurement. If Window == null, the CollectionView frame is
set directly to the given constraints (clamping ∞ to 10000 to match
UIKit’s UILayoutFittingExpandedSize). Then SetNeedsLayout() and
LayoutIfNeeded() are called so UIKit computes the real content size. The
original frame is always restored in a finally block. No AddSubview,
RemoveFromSuperview, or ReloadData() is used. This allows Measure() to
return the correct height even before the control is mounted.
 
**Reason for Mac behavior:**
 
On macOS via MacCatalyst, UISheetPresentationController always presents
sheets as full-height modal panels. Custom detents (custom heights) are
ignored by the platform.

### Issues Fixed
Fixes #32983

### Tested the behaviour in the following platforms
- [x] iOS
- [x] Mac
- [ ] Android
- [ ] Windows

### Output Screenshot
Before Issue Fix | After Issue Fix |
|----------|----------|
|<video width="100" height="100" alt="Before Fix"
src="https://github.com/user-attachments/assets/8f31b5fe-6482-490c-b6f4-c77376257042">|<video
width="100" height="100" alt="After Fix"
src="https://github.com/user-attachments/assets/d0b2e698-8843-4d9b-a773-0bd3db427b7c">|
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 messes up Measure operation on Views

6 participants