[iOS/MacCatalyst] Fix CollectionView cell misalignment regression on candidate branch#34667
[iOS/MacCatalyst] Fix CollectionView cell misalignment regression on candidate branch#34667kubaflo merged 8 commits intoinflight/candidatefrom
Conversation
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
🤖 AI Summary📊 Expand Full Review —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34667 | Remove _collectionViewDescendant; inject CellSafeAreaOverride from TemplatedCell before Arrange; ContentInsetsReference=None in LayoutFactory2 |
❌ FAILED (Gate) | MauiView.cs, TemplatedCell.cs, TemplatedCell2.cs, LayoutFactory2.cs |
Gate failed: new snapshot test has no reference |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | UICollectionViewCell Superview short-circuit by walking UIKit Superview chain for UICollectionViewCell ancestor | Detection PASS (40 passed, 0 failed, 4 ignored) | MauiView.cs (+28 lines), LayoutFactory2.cs (+5 lines) |
Uses UIKit hierarchy; no parallel code paths |
| 2 | try-fix (claude-sonnet-4.6) | Expand add ancestry check alongside check in | detection PASS (80 passed, 0 failed) | MauiView.cs (+5/-1 lines) |
Minimal single-file; fixes timing at earliest decision point |
| 3 | try-fix (gpt-5.3-codex) | Override to invalidate + | PASS (40 passed, 0 failed, 4 ignored) | MauiView.cs (+9 lines) |
Lifecycle-based; no detection logic change |
| 4 | try-fix (gpt-5.4) | Call from TemplatedCell/TemplatedCell2 before Arrange | PASS (160 passed, 0 failed) | TemplatedCell.cs, TemplatedCell2.cs |
Cell-side fix at arrange call site |
| 5 | try-fix (claude-sonnet-4.6, round 2) | Reset in alongside existing | PASS (200 passed, 0 failed) | MauiView.cs (+1 line) |
Minimal: clears stale scroll-view-descendant cache when safe area fires; mirrors existing invalidation pattern |
| PR | PR #34667 | Remove _collectionViewDescendant; inject CellSafeAreaOverride from TemplatedCell before Arrange; ContentInsetsReference=None in FAILED (Gate) |
MauiView.cs, TemplatedCell.cs, TemplatedCell2.cs, LayoutFactory2.cs |
Gate failed: new snapshot test has no committed reference | LayoutFactory2 |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| led to Attempt 5 | |||
| became Attempt 5 | |||
| gpt-5.3-codex | 2 | YES | Defer/don't cache when Window == null; async recompute |
| claude-opus-4.6 | 3 | NO NEW IDEAS | Attempt 5 is optimal: +1 line is theoretical minimum for a caching bug |
| claude-sonnet-4.6 | 3 | Marginal | Check scroll view TYPE (not UICollectionView/ more complex than Attempt 5 |
| gpt-5.3-codex | 3 | Marginal | Also add _scrollViewDescendant = null to InvalidateSafeArea() (+2 lines broader but not needed given tests pass |
Exhausted: Yes (3 rounds reached; no new ideas that beat Attempt 5)
Selected Fix: Attempt _scrollViewDescendant = null in SafeAreaInsetsDidChange() (+1 line, MauiView.cs only)5
Reason: Attempt 5 is the most minimal correct fix: one line in MauiView.cs that mirrors the existing _parentHandlesSafeArea = null reset pattern in the same method. It targets the root cause precisely (stale _scrollViewDescendant cache never cleared when SafeAreaInsetsDidChange fires) with zero new code paths, zero new properties, and zero changes to TemplatedCell or LayoutFactory2. All 200 CollectionView device tests pass.
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issues #33604, #34635; iOS/MacCatalyst; 5 impl files + 1 device test |
| Gate | ❌ FAILED | CollectionViewItemsShouldRespectSafeAreaEdges — new snapshot test has no committed reference; passes on broken branch |
| Try-Fix | ✅ COMPLETE | 5 attempts (all PASS), cross-pollination exhausted (3 rounds) |
| Report | ✅ COMPLETE |
Summary
PR #34667 fixes a p/0 regression (#34635) and related issue #33604 where CollectionView cells on iOS/MacCatalyst incorrectly apply safe area insets due to a timing race in MauiView._parentHandlesSafeArea/_scrollViewDescendant caching. The gate failed because the new snapshot test CollectionViewItemsShouldRespectSafeAreaEdges has no committed reference screenshot, causing it to pass on the broken baseline (test creates its own reference). A simpler and more correct alternative fix was found via Try-Fix.
Root Cause
PR #33908 added a _collectionViewDescendant exception that used Window.SafeAreaInsets as the safe area base for CollectionView cells. On macCatalyst, Window.SafeAreaInsets.Top is 41px (title bar). Combined with a timing race where _scrollViewDescendant is cached as false during the initial UICollectionView layout pass (before the cell is in the full hierarchy), RespondsToSafeArea() returns true and cells apply _appliesSafeAreaAdjustments = true with a 41px top inset — causing all items to be misaligned.
The fundamental issue: _scrollViewDescendant is cached but never cleared when SafeAreaInsetsDidChange() fires. The stale false value persists through subsequent layout passes.
Fix Quality
PR's fix concerns:
- Gate ❌ FAILED:
CollectionViewItemsShouldRespectSafeAreaEdgessnapshot test has no committed reference — it passes on the broken baseline, making it invalid as a gate test. The snapshot reference needs to be committed before this test can validate the fix. - Adds a new
CellSafeAreaOverrideinternal property,ApplyCellSafeAreaOverride()static method, andComputeCellSafeAreaInsets()with geometry-based per-cell inset computation — a parallel safe area code path alongside_safeArea/_appliesSafeAreaAdjustments ComputeCellSafeAreaInsets()still usesWindow.SafeAreaInsetsat LayoutSubviews time — on macCatalyst, this includes the 41px title bar; for mixed-edge configurations the issue could recur- New device test only checks
AppliesSafeAreaAdjustments == false, not that correct insets are applied - Modifies 4+ files; introduces complexity in both TemplatedCell files
ContentInsetsReference = UIContentInsetsReference.None(iOS 26.1+ fix) is bundled — should be evaluated independently
Selected Alternative — Attempt 5 (+1 line in MauiView.cs):
public override void SafeAreaInsetsDidChange()
{
+ _scrollViewDescendant = null;
_safeAreaInvalidated = true;
_parentHandlesSafeArea = null;
base.SafeAreaInsetsDidChange();- Single file, single line addition
- Mirrors the existing
_parentHandlesSafeArea = nullreset — consistent with established pattern - No new properties, no new code paths, no changes to TemplatedCell
- Fixes the root cause: stale
_scrollViewDescendantcache is now cleared whenSafeAreaInsetsDidChangefires; nextRespondsToSafeArea()call walks a fully-connected hierarchy and correctly returnsfalse - 200 CollectionView device tests pass
Recommendation: Replace the PR's implementation with Attempt 5. Also consider:
- Whether
ContentInsetsReference = UIContentInsetsReference.Nonein LayoutFactory2 is separately needed for iOS 26.1+ — it could be added alongside Attempt 5 if confirmed necessary - Fix the gate test: commit a reference screenshot or convert
CollectionViewItemsShouldRespectSafeAreaEdgesto a non-snapshot assertion
Attempt 5 fails to resolve #34635 as the root cause is a fundamental coordinate mismatch, not a stale cache; Window.SafeAreaInsets incorrectly reports a constant 41px on macCatalyst. Current fix resolves this by geometrically computing per-cell insets in TemplatedCell.LayoutSubviews, with the ContentInsetsReference override now strictly conditioned for iOS 26+. |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
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!
Reverts the iOS-specific
Window.SafeAreaInsetschanges that caused CollectionView cell misalignment on the candidate branch from #33908 , and re-implements safe area handling via per-cell geometric overlap computation that works correctly across all platforms.Root Cause
Cause of regression:
PR #33908 introduced the issue by using
Window.SafeAreaInsetsforCollectionViewcells; on macOS this includes a 41px title bar. Due to a timing race (IsParentHandlingSafeArea()returning false), all cells incorrectly applied this inset, causing misalignment.Reverted the iOS changes from PR #33908 in MauiView.cs, restoring the previous behavior. Android changes were retained as they function correctly.
Actual root cause of Issue33604:
PR #33908 modified
MauiView.GetAdjustedSafeAreaInsets()to useWindow.SafeAreaInsetsas the base safe area for CollectionView cell descendants. On macOS,Window.SafeAreaInsets.Topis 41px (title bar). Combined withIsParentHandlingSafeArea()returning false due to a timing race (parent not yet validated), every CollectionView cell received_appliesSafeAreaAdjustments = truewith a 41px top inset, causing all items to be misaligned on MacCatalyst.Fix: Fully reverted all iOS changes from PR #33908 in MauiView.cs, restoring it to the pre-#33908 state. The Android changes from PR #33908 are retained as they work correctly (WindowInsetsCompat propagates to all views regardless of
RecyclerViewcontainment).Description of Change
The fix introduces a shared helper in
MauiView(ApplyCellSafeAreaOverride/ComputeCellSafeAreaInsets) used by bothTemplatedCell(CV1) andTemplatedCell2(CV2) duringLayoutSubviews.Before arranging, per-cell safe area insets are computed based on the cell's geometric position relative to the window's unsafe regions and passed via a new
CellSafeAreaOverrideproperty. The cell frame remains full-width so backgrounds extend edge-to-edge, while content is inset internally.Uniform configurations (
Container×4,None×4,All×4) are skipped as they are handled by the parent layout. Insets are applied only for edges explicitly set toContainerorAll, and ignored forSoftInput-only edges. A tolerance filter (ToSafeAreaInsets) avoids sub-pixel layout noise.MauiViewapplies this override duringCrossPlatformMeasureandCrossPlatformArrangewhen_appliesSafeAreaAdjustmentsis false, treating it as internal padding consistent with existingAdjustForSafeAreabehavior. Stale overrides are cleared during cell reuse when the template no longer implementsISafeAreaView2.Additionally,
ContentInsetsReferenceis set toUIContentInsetsReference.Noneat all three section creation sites inLayoutFactory2(Linear, Grid, Carousel). On iOS 26.1+, the default (.automatic→.safeArea) started actively insetting cells at the section level inUICollectionViewCompositionalLayout, conflicting with MAUI's internal safe area handling. Setting.Nonerestores pre-26.1 behavior. CV1 is unaffected as it usesUICollectionViewFlowLayout, which does not have this property.Issues Fixed
Fixes #33604
Fixes #34635
Additional Fixes
Also resolves the following Failed iOS test cases:
VerifyGroupFooterTemplate_WithFooterStringVerifyGroupHeaderTemplate_WithFooterStringVerifyIsGroupedFalse_WithHeaderAndFooterStringSelectedItemVisualIsClearedCollectionViewItemsShouldRespectSafeAreaEdgesTested the behaviour in the following platforms
Screenshots
Issue33604
iOS-withoutfix.1.mov
iOS-withfix.1.mov
Issue34635
BeforeFix.mov
AfterFix.mov