Skip to content

[iOS] NavigationPage: Fix large-title collapse for wrapped scroll content - #36511

Merged
kubaflo merged 11 commits into
inflight/currentfrom
jfversluis-fix-ios-large-title-nonshell
Aug 9, 2026
Merged

[iOS] NavigationPage: Fix large-title collapse for wrapped scroll content#36511
kubaflo merged 11 commits into
inflight/currentfrom
jfversluis-fix-ios-large-title-nonshell

Conversation

@jfversluis

@jfversluis jfversluis commented Jul 10, 2026

Copy link
Copy Markdown
Member

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!

Description of Change

Fixes non-Shell NavigationPage large titles on iOS 26 when the page's scrollable content is wrapped by a Grid, ContentView, or another layout.

Root Cause

UIKit collapses a large navigation title by observing a direct, visible UIScrollView owned by the page controller. When MAUI content wraps a ScrollView, ListView, CollectionView, TableView, or WebView, the native scroller is nested below another view, so UIKit does not react to its offset.

Fix

On iOS 26 only, wrapped scrollers are mirrored through a transparent, non-interactive direct-child UIScrollView behind the real content. The proxy:

  • observes the real native scroller with KVO without replacing CollectionView's delegate;
  • caches the resolved native scroller while it remains attached, avoiding repeated managed/native tree discovery during layout;
  • skips hidden branches and horizontal controls, then chooses the visible vertical candidate nearest the top edge;
  • resolves controller-wrapped Items2 CollectionView platform views and WKWebView.ScrollView explicitly;
  • supports wrapped ScrollView, ListView, CollectionView, TableView, and WebView content;
  • mirrors content offset, adjusted inset, and the nested scroller's complete scroll range;
  • uses the resting -AdjustedContentInset offset when observation is armed, so restored or OnAppearing programmatic offsets collapse correctly;
  • is created only after appearance and only once scrolling requires it, preserving the initially visible large title;
  • is removed and its observation disposed with the page lifecycle.

Direct root scrollers continue using UIKit's native behavior. Hidden navigation bars and app safe-area settings are not modified.

Regression Coverage

The iOS UI suite covers:

  • direct and Grid/ContentView-wrapped ScrollView;
  • late-bound wrapped content;
  • ListView, Items2 CollectionView, TableView, and WebView;
  • hidden and horizontal scrollers preceding the visible vertical scroller;
  • fixed siblings and a short nested viewport boundary;
  • explicit post-appearance CollectionView.ScrollTo;
  • OnAppearing/restored programmatic offsets;
  • hidden-navigation-bar safe-area preservation;
  • initial expanded-title and collapsed-title visual baselines.

What NOT to Do

  • Do not change SafeAreaEdges to force UIKit large-title behavior; that moves fixed siblings and changes unrelated app layout.
  • Do not subscribe to UIScrollView.Scrolled for CollectionView; event registration replaces its existing native delegate and crashes.
  • Do not create the proxy during initial layout offset changes; it can make the initial large title render transparent.
  • Do not recursively rediscover the native scroller on every layout once a stable attached candidate is known.

Verification

  • Reproduced the original failure on iPhone 11 Pro, iOS 26.1 Simulator.
  • Final targeted run: BuildAndRunHostApp.ps1 -Platform ios -TestFilter "FullyQualifiedName~Issue33037NonShell" -DeviceUdid CB7862CA-0D12-426D-A7E2-C8F80723B32F14/14 passed.
  • Captured before/after simulator screenshots and video for the broken and fixed builds.
  • Final MAUI expert review found no remaining major or moderate correctness issues.

Existing PR Comparison

PR #33246 fixed the Shell path. This PR covers the remaining non-Shell NavigationPage path.

Issues Fixed

Fixes #33037

jfversluis and others added 6 commits July 10, 2026 16:42
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Ensure non-Shell NavigationPage pages with root layout wrappers around scrollable content can still collapse large titles into a visible standard title on iOS 26.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Handle default content wrappers when applying the iOS 26 non-Shell large-title root-layout safe-area adjustment and invalidate affected descendant safe-area caches.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings July 10, 2026 16:58
@jfversluis
jfversluis temporarily deployed to copilot-pat-pool July 10, 2026 16:58 — with GitHub Actions Inactive
@github-actions

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

Or

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

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

This PR completes the iOS 26 large-title collapse fix for non-Shell NavigationPage scenarios by ensuring the collapsed (standard) title remains visible, and by adjusting safe-area behavior for iOS 26 translucent navigation bars in specific non-Shell layouts. It also adds UITest coverage to prevent regressions across several common non-Shell content hierarchies.

Changes:

  • Sync UIViewController.Title with NavigationItem.Title so the standard title remains visible after a large-title collapse in non-Shell navigation.
  • Re-evaluate/adjust safe-area handling for iOS 26 translucent non-Shell NavigationPage pages (including invalidating safe-area caches when SafeAreaEdges changes).
  • Add iOS UITest + HostApp scenarios covering ScrollView/ListView/CollectionView and common wrapper layouts, with iOS 26 visual baselines.
Show a summary per file
File Description
src/Core/src/Handlers/View/ViewHandler.iOS.cs Invalidates iOS safe-area caches when SafeAreaEdges updates so runtime changes re-evaluate correctly.
src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs Fixes non-Shell title collapse visibility by syncing controller title; adds iOS 26 large-title safe-area adjustment + invalidation logic.
src/Compatibility/Core/src/iOS/Renderers/NavigationRenderer.cs Mirrors the title-sync fix for the legacy compatibility renderer path.
src/Controls/tests/TestCases.HostApp/Issues/Issue33037NonShell.cs Adds HostApp repro/coverage pages for multiple non-Shell scenarios (direct scrollables + wrapped layouts).
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33037NonShell.cs Adds iOS UITest regression coverage with screenshots after scrolling for each non-Shell scenario.

Copilot's findings

  • Files reviewed: 5/10 changed files
  • Comments generated: 0

@jfversluis

Copy link
Copy Markdown
Member Author

@AlleSchonWeg the PR artifacts should become available from the checks on this PR. Please test them when you get a chance and let us know whether they resolve the remaining non-Shell NavigationPage large-title scenarios. Thank you!

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jul 10, 2026
@MauiBot MauiBot added s/agent-gate-failed AI could not verify tests catch the bug 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 Jul 10, 2026
MauiBot

This comment was marked as outdated.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jul 10, 2026
Keep the non-Shell iOS 26 large-title workaround scoped to the top safe area so bottom and side container insets continue to apply.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@kubaflo

kubaflo commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

/azp run

@MauiBot MauiBot added 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 Aug 2, 2026
@MauiBot

This comment has been minimized.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Aug 2, 2026
@kubaflo

kubaflo commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

/review -b improved-reviewer -p ios

1 similar comment
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Aug 5, 2026
@MauiBot MauiBot added s/agent-review-incomplete AI review did not complete all expected phases and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels Aug 5, 2026

@MauiBot MauiBot 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.

Expert Review — 9 findings

See inline comments for details.

Comment thread src/Controls/tests/TestCases.HostApp/Issues/Issue33037NonShell.cs
Comment thread src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33037NonShell.cs Outdated
Comment thread src/Core/src/Handlers/View/ViewHandler.iOS.cs Outdated
@MauiBot

This comment has been minimized.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Aug 6, 2026

@MauiBot MauiBot 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.

Expert Review — 11 findings

See inline comments for details.

Comment thread src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33037NonShell.cs Outdated
Comment thread src/Core/src/Handlers/View/ViewHandler.iOS.cs Outdated
Comment thread src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33037NonShell.cs Outdated
Comment thread src/Compatibility/Core/src/iOS/Renderers/NavigationRenderer.cs Outdated
@MauiBot

MauiBot commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

AI Review Summary

@jfversluis — new AI review results are available based on this last commit: 08208d6.

Gate Passed Confidence Low Platform iOS


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ✅ PASSED

Platform: IOS · Base: main · Merge base: 8266dc21

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ Issue33037NonShell Issue33037NonShell ✅ FAIL — 340s ✅ PASS — 152s
🔴 Without fix — 🖥️ Issue33037NonShell: FAIL ✅ · 340s

Error-relevant lines (filtered from the build log):

     at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
   at Microsoft.Maui.TestCases.Tests.Issues.Issue33037NonShell.LargeTitleCollapsesToVisibleStandardTitle(String buttonId, String scrollerId, String title, String scenarioName) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33037NonShell.cs:line 45
   at InvokeStub_Issue33037NonShell.LargeTitleCollapsesToVisibleStandardTitle(Object, Span`1)
   at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
🟢 With fix — 🖥️ Issue33037NonShell: PASS ✅ · 152s

(no coded error found; showing last 1200 chars)

sesToVisibleStandardTitle Stop
  Passed LargeTitleCollapsesToVisibleStandardTitle("Issue33037DynamicContentViewGridScrollViewButton","Issue33037DynamicContentViewGridScrollViewScroller","Issue33037 Dynamic","DynamicContentViewGridScrollView") [7 s]
>>>>> 8/5/2026 6:00:06 PM LargeTitleCollapsesToVisibleStandardTitle Start
>>>>> 8/5/2026 6:00:16 PM LargeTitleCollapsesToVisibleStandardTitle Stop
  Passed LargeTitleCollapsesToVisibleStandardTitle("Issue33037ListViewButton","Issue33037ListViewScroller","Issue33037 List","ListView") [10 s]
>>>>> 8/5/2026 6:00:16 PM LargeTitleCollapsesToVisibleStandardTitle Start
>>>>> 8/5/2026 6:00:25 PM LargeTitleCollapsesToVisibleStandardTitle Stop
  Passed LargeTitleCollapsesToVisibleStandardTitle("Issue33037CollectionViewButton","Issue33037CollectionViewScroller","Issue33037 Collection","CollectionView") [8 s]
NUnit Adapter 4.5.0.0: Test execution complete
Results File: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue33037NonShell.trx
Test Run Successful.
Total tests: 6
     Passed: 6
 Total time: 1.1454 Minutes
>>> TRX_RESULT_FILE: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue33037NonShell.trx
📁 Fix files reverted (3 files)
  • src/Compatibility/Core/src/iOS/Renderers/NavigationRenderer.cs
  • src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs
  • src/Core/src/Handlers/View/ViewHandler.iOS.cs

📋 Pre-Flight — Context & Validation

Issue: #33037 - iOS Large Title display disappears
PR: #36511 - Fix iOS non-Shell large title collapse
Platforms Affected: iOS 26+ and MacCatalyst 26+
Files Changed: 3 implementation, 8 test

Key Findings

  • The regression affects non-Shell NavigationPage large titles on iOS 26: scrolling should collapse the large title into the standard navigation title, but the title disappears or does not collapse when safe-area handling keeps scrollable content below the translucent navigation bar.
  • The linked issue reports that a direct ScrollView can work with a transparent/translucent navigation bar, while a Grid containing a ScrollView, a root ListView, and a root CollectionView remain broken.
  • The PR synchronizes UIViewController.Title, conditionally applies handler-specific SafeAreaEdges to a root layout containing scrollable content, invalidates iOS safe-area caches, and adds six iOS visual-regression scenarios.
  • The review found unresolved risks: applying Top = None to the whole root layout can place fixed siblings under the navigation bar; descendant removal can trigger full-tree walks while CollectionView cells recycle; pre-iOS-26 snapshots are absent; and several fixtures do not execute the safe-area path they claim to cover.
  • The supplied gate result is authoritative: the tests fail without the PR fix and pass with it. Gate verification was not rerun.

Code Review Summary

Verdict: NEEDS_CHANGES
Confidence: low
Errors: 4 | Warnings: 3 | Suggestions: 2

Key code review findings:

  • ✗ Applying (Container, None, Container, Container) to an entire root layout also removes the top inset from non-scrolling siblings such as fixed headers (src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs:2341).
  • HasScrollableAncestor cannot filter descendant removals after the removed child's parent has already been cleared, causing repeated full-tree scans during CollectionView cell recycling (src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs:1702).
  • ✗ The UI test runs on all iOS versions but adds snapshots only under snapshots/ios-26, so pre-iOS-26 lanes have no matching baselines (src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33037NonShell.cs:45).
  • ✗ Several fixtures cannot discriminate the safe-area regression: direct root ListView and CollectionView do not yield a root layout, while direct ScrollView traversal reaches its inner layout rather than treating the scroller as the safe-area owner.
  • GetRootLayout descends through ScrollView and can mutate a layout inside the native scroll view.
  • LargeTitleDisplayMode.Automatic is treated as eligible without resolving inherited native state.
  • ⚠ MacCatalyst 26 behavior is enabled without MacCatalyst test coverage.
  • ℹ Prefer central descendant safe-area invalidation in Core over renderer-specific recursive invalidation.
  • ℹ Consider separating the unrelated UIViewController.Title synchronization from the safe-area fix.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #36511 Track a root layout containing scrollable descendants, apply handler-specific top safe-area opt-out on iOS/MacCatalyst 26, synchronize the native controller title, and invalidate safe-area caches ✅ PASSED (Gate) 3 implementation files Original PR

🔬 Code Review — Deep Analysis

Code Review — PR #36511

Independent Assessment

What this changes: Three things bundled together.

  1. ParentingViewController (non-Shell iOS NavigationRenderer) gains a large-title safe-area subsystem: on iOS/MacCatalyst 26+ with a translucent bar + PrefersLargeTitles, it walks ContentPage.Content for a "root layout" (Microsoft.Maui.Controls.Layout), and if that subtree contains a ScrollView/ListView/ItemsView it writes SafeAreaEdges = (Container, None, Container, Container) at SetterSpecificity.FromHandler so the layout extends under the bar and iOS can collapse the large title. It tracks the layout by WeakReference + PropertyChanged, tracks Child.DescendantAdded/Removed, and restores on child change / Disconnect.
  2. UpdateTitle now sets UIViewController.Title alongside NavigationItem.Title (in both the new and the legacy src/Compatibility renderer).
  3. ViewHandler.MapSafeAreaEdges now calls InvalidateSafeArea() on MauiView/MauiScrollView — a Core behavior change on every iOS view.

Inferred motivation: iOS 26 changed nav-bar/safe-area interaction; unless the page's root layout stops consuming the top inset, the scroller never scrolls "under" the bar, so the large title never collapses and the standard title never appears.

Reconciliation with PR Narrative

Author claims: Completes the iOS 26 large-title fix for non-Shell NavigationPage (companion to #33246 for Shell); syncs UIViewController.Title; invalidates safe-area caches; adds UI coverage for direct ScrollView, Grid+ScrollView, ContentView+Grid+ScrollView, ListView, CollectionView with iOS 26 baselines; claims 5/5 passed.

Agreement/disagreement:

  • Agree on mechanism and on the Shell/non-Shell split.
  • Disagree on coverage. The test declares 6 TestCases, but verification reports 5/5 — one case is unaccounted for.
  • Disagree that the listed scenarios exercise the safe-area fix. GetRootLayout bails to null for Content = ListView and Content = CollectionView (neither is MauiLayout nor IContentView), and for Content = ScrollView it recurses through the scroller (IScrollView : IContentView, IContentView.PresentedContent => ContentScrollView.cs:447) and returns the inner stack, which then has no scrollable content. So 3 of the 6 fixtures never reach the new code; they can only be validated by the unrelated UIViewController.Title change. The description implies otherwise.
  • Agree the specificity design is careful: ClearValue(..., FromHandler) cannot clobber a user value (BindableObject.cs:132-146), and a later user SetValue evicts the handler value (BindableObject.cs:642-645).

Prior Review Reconciliation

Latest MauiBot round is 2026-08-06T00:40:56Z against commit_id 08208d6fthe current head SHA, with no subsequent commits. All findings below are therefore unresolved.

Prior ❌ Error Finding Source Status Evidence
[major] HasScrollableContent un-insets the whole root layout, so a visible fixed header sibling renders under the translucent bar MauiBot, NavigationRenderer.cs:2341 Unresolved Confirmed: LargeTitleRootLayoutSafeAreaEdges = (Container, **None**, Container, Container) → Top=None; EdgesForExtendedLayout = All when translucent (line ~1573)
[major] HasScrollableAncestor is dead on the remove path → full tree walk per recycled cell while scrolling MauiBot, NavigationRenderer.cs:1702 Unresolved Confirmed: Element.OnChildRemoved calls child.SetParent(null) before OnDescendantRemoved (Element.cs:681-689); TemplatedCell2.cs:97/245 remove logical children per recycle; OnDescendantRemoved bubbles via RealParent (Element.cs:984-988)
[major] Baselines committed only to snapshots/ios-26/ while the test is gated on #if IOS with no runtime version check → pre-26 lane fails on missing baseline MauiBot, Issue33037NonShell.cs:45 Unresolved Confirmed: UITest.cs:356-386 picks environmentName = "ios" below 26; VisualRegressionTester.cs:72/85 Fail("Baseline snapshot not yet created…"); only 8 files exist solely in ios-26, 6 of them from this PR
[major] Fixtures cannot discriminate the regression — the only non-scrolling sibling is IsVisible = false MauiBot, Issue33037NonShell.cs:134 (HostApp) Unresolved Confirmed at Issue33037NonShell.cs Issue33037GridActivityIndicator
CHANGES_REQUESTED (human) kubaflo, 2026-07-11 Not dismissed Latest review state on the PR is CHANGES_REQUESTED (MauiBot, 2026-08-02)

Per Rule #5, these override my own assessment.

Blast Radius Assessment

Triggered by: handler/platform plumbing, PropertyChanged subscriptions, and the CollectionView / Toolbar-Navigation frequently-regressed families.

  • Runs for all instances: Partially. ViewHandler.MapSafeAreaEdges runs for every iOS view on every OS version — new InvalidateSafeArea() on the mapper path. It is cheap (_safeAreaInvalidated = true; _parentHandlesSafeArea = null; SetNeedsLayout()MauiView.cs:878), but it is unconditional. The renderer subsystem is gated on iOS/Catalyst 26 + translucent + PrefersLargeTitles, so non-26 devices short-circuit before any tree walk. However, DescendantAdded/Removed subscriptions are added for every NavigationPage child page on iOS 26 regardless of large-title usage.
  • Startup impact: No new static mutable state; LargeTitleRootLayoutSafeAreaEdges is a static readonly struct. No startup crash risk found.
  • Static/shared state: No. Per-ParentingViewController state, tracked layout held weakly; the only strong edge (layout → VC via PropertyChanged) is released in RestoreLargeTitleSafeArea, reached from the Child setter and Disconnect. No leak found.

CI Status

gh pr checks was unavailable (gh unauthenticated, exit 4 — tool-unavailable fallback applied). Recovered state via the unauthenticated GitHub check-runs API on head 08208d6f:

  • Required-check result: maui-pr → FAILURE. "Build #10.0.100-ci+pr.36511.azdo.1509871 had test failures", 1 error. Failing leg: Run Helix Unit Tests Windows Helix Unit Tests (Debug). Build Analysis also failure. Combined commit status: pending.
  • Classification: probably-unrelated but unconfirmed. The Release twin of the same Helix leg passed and no iOS/macOS/Android leg failed, while this PR touches only iOS-conditional code (ViewHandler.iOS.cs, iOS renderers) plus iOS test assets — Windows Helix unit tests don't compile or exercise those paths. I could not confirm this: AzDO build 1509871 returned 404 anonymously, so azdo-build-investigator could not complete a root-cause classification.
  • Coverage gap: UI tests (maui-pr-uitests) did not run on this PR, so the only tests validating this change never executed in CI.
  • Action taken: attempted gh → failed; fell back to GitHub check-runs API; attempted AzDO timeline/test APIs → 404. Confidence capped at low (CI red and no relevant tests run).

Findings

❌ Error — Snapshot baselines exist only for ios-26; the pre-26 iOS lane will fail

Issue33037NonShell.cs:16-24 gates only on #if IOS, with no HelperExtensions.IsIOS26OrHigher check (the convention used in ClipFeatureTests.cs:23, StepperFeatureTests.cs:300). UITest.cs:363 resolves environmentName = "ios" on pre-26 simulators, and VisualRegressionTester.cs:72 hard-fails on a missing baseline. Since the fix itself is a no-op below iOS 26, either Assert.Ignore below 26 or commit snapshots/ios/ baselines.

❌ Error — Whole-root Top = None strips the inset from non-scrolling siblings

NavigationRenderer.cs HasScrollableContent returns true if any descendant scrolls, but LargeTitleRootLayoutSafeAreaEdges is then applied to the entire root layout. For the very common Grid RowDefinitions="Auto,*" { header; CollectionView }, the header is arranged from y=0 of the extended page view and renders under the translucent bar. Narrow the precondition to "the child occupying the top edge is itself the scroller."

❌ Error — HasScrollableAncestor never filters removals → tree walk on the scroll hot path

Element.OnChildRemoved nulls the parent before raising DescendantRemoved, so the ancestor guard always returns false for removals. With any MauiLayout/IContentView-rooted ItemTemplate, every recycled CollectionView cell (TemplatedCell2.cs:97/245) triggers GetRootLayout + a full recursive HasScrollableContent walk — while scrolling, on exactly the scenario this PR targets. The add path is filtered correctly, and the current fixture's bare-Label template masks it.

⚠️ Warning — GetRootLayout descends through a ScrollView

IScrollView : IContentView and ScrollView.cs:447 exposes PresentedContent => Content, so the walk enters the scroller. For ScrollView { Grid { CollectionView } } the override lands on a layout inside a UIScrollView. The write is largely inert (MauiView._scrollViewDescendant) but still mutates a user-visible bindable and flips HasExplicitSafeAreaEdges to true for that layout. The bail-out should stop at an active safe-area owner, not only at one with explicit edges.

⚠️ Warning — LargeTitleDisplayMode.Automatic is not resolved

ShouldExtendRootLayoutUnderNavigationBar rejects only UINavigationItemLargeTitleDisplayMode.Never. Automatic (the MAUI default, see UpdateLargeTitles) inherits from the previous view controller, so a page pushed after a Never page gets an inline title and a stripped top safe area.

⚠️ Warning — MacCatalyst 26 is enabled with zero coverage

NeedsLargeTitleSafeAreaTracking() returns true for IsMacCatalystVersionAtLeast(26), but the test is #if IOS and the Mac test project defines MACCATALYST, not IOS. Catalyst has a different UseSafeArea default (true) and different bar metrics — historically the exact place safe-area fixes regress. Either add #if MACCATALYST coverage or restrict the gate to iOS.

💡 Suggestion — MapSafeAreaEdges should invalidate descendants in Core

ViewHandler.iOS.cs:157 invalidates only the view itself, yet descendants cache _parentHandlesSafeArea. The PR works around this with a hand-rolled recursive walk in the compatibility renderer — which duplicates the same type switch and stops at ItemsView/ListView platform views. A single internal Core helper (recursive) would fix every caller, not just this one.

💡 Suggestion — Split the UIViewController.Title change out

UpdateTitle is unrelated to safe area, untested, and duplicated into the deprecated src/Compatibility renderer. UIViewController.Title also feeds UITabBarItem.Title and back-button derivation, so it can change titles in TabbedPage-hosted scenarios the new tests don't cover.

Failure-Mode Probing

  • Repeat call while the _updatingLargeTitleSafeArea latch is set (Rule [Spec] Transitions #6 trace): set in UpdateLargeTitleSafeArea only, cleared in its finally — no path leaves it stuck. SetValueFromRenderer/ClearValue raise PropertyChanged synchronously → HandleLargeTitleRootLayoutPropertyChanged → suppressed, but that re-entrant call is redundant with the in-flight one. RestoreLargeTitleSafeArea (the one clear path invoked outside the latch, from the Child setter) unsubscribes before ClearValue, so it can't re-enter. InvalidateSafeArea only sets flags + SetNeedsLayout, so no synchronous tree mutation can be dropped inside the window. No lost update found — latch is sound.
  • _largeTitleSafeAreaApplied desync: if something else writes SafeAreaEdges, the flag self-corrects (applied && SafeAreaEdges != Large → applied = false). Because FromHandler is evicted by any non-handler SetValue (BindableObject.cs:642-645) and is top-specificity, the flag and the stored value cannot disagree. Disproven as a failure mode.
  • Handler disconnect / Shell tab switch / pop: Disconnect(bool)Child = null → setter → RestoreLargeTitleSafeArea() → unsubscribes the layout PropertyChanged (the only strong layout→VC edge) and clears the override. DescendantAdded/Removed are -= unconditionally even though += is gated — harmless. No leak.
  • Scrollable removed at runtime: DescendantRemoved for a ScrollView/MauiLayout passes CanAffectLargeTitleSafeArea (the ancestor filter being dead here actually helps), so the override is correctly restored.
  • IsVisible = false scroller: HasScrollableContent ignores visibility, so a hidden scroller still opts the whole root out of the top inset, permanently. Not covered by any fixture.
  • Null Handler/PlatformView: InvalidateSafeArea(IView) uses view.Handler?.PlatformView is … and GetRootLayout null-guards Child/PresentedContent. No NRE path found.
  • Page-level SafeAreaEdges: the no-arg GetRootLayout() never checks Child's own HasExplicitSafeAreaEdges, so <ContentPage SafeAreaEdges="All"> is not honored as a veto. In practice the page then handles the top inset and IsParentHandlingSafeArea suppresses the child, so the visual impact is likely nil — noting it rather than escalating.

Verdict: NEEDS_CHANGES

Confidence: low — capped three ways: platform-specific handler/UI plumbing (max medium), CI red (maui-pr failure, max low), and no relevant tests run in CI (UI tests skipped on PR builds, max low). I could not classify the Helix failure because AzDO build 1509871 is inaccessible anonymously.

Summary: The core design is genuinely careful — the re-entrancy latch, the SetterSpecificity.FromHandler round-trip, and the subscription lifecycle all survive close tracing, and I found no leak or stuck-state defect. But four [major] findings from the prior MauiBot round sit unresolved against the current head SHA 08208d6f, and I independently confirmed each: the whole-root Top = None will push a visible fixed header under the translucent bar, the removal-path ancestor filter is dead so every recycled CollectionView cell triggers a tree walk mid-scroll, the ios-26-only baselines will fail the pre-26 lane, and the fixtures can't discriminate the regression they introduce. Independently: 3 of the 6 fixtures never reach the new code at all (ListView/CollectionView return null from GetRootLayout; ScrollView is walked through), the verification note says 5/5 for a 6-case test, MacCatalyst 26 ships uncovered, and a human CHANGES_REQUESTED from kubaflo is still open. Per Rules #5 and #6 this is NEEDS_CHANGES regardless of my own assessment.


🛠️ Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix Explicitly designate UIKit's top-edge content scroll view using a weakly cached, bounded native-view resolver ❌ FAIL (3/6) 1 file Collapse worked in all six cases, but wrapped-content screenshots failed because content still did not extend under the bar
2 try-fix Transfer consumed top safe-area geometry to one structurally top-flush ScrollView during platform arrange ❌ FAIL (3/6) 4 files Two runtime iterations reproduced Candidate #1's exact wrapped-case snapshot differences; handoff had no observable geometry effect
PR PR #36511 Apply handler-specific top safe-area opt-out to a root layout containing scrollable content, recursively invalidate safe-area caches, and synchronize the native controller title ✅ PASSED (Gate) 3 files Original PR; unresolved whole-root geometry and descendant-removal hot-path concerns

Cross-Pollination

Model Round New Ideas? Details
claude-opus-5 1 Yes Combine proven UIKit scroll-view designation with a narrowly scoped geometry adjustment that affects only the designated scroller path, not the entire root layout
gpt-5.6-sol 1 Yes Transfer only the consumed top geometry to a structurally top-flush ScrollView during platform arrange
Both 2 Skipped The bounded Phase 2 budget was reached after two complete expert-review, implementation, and iOS test cycles

Exhausted: Yes — both required model families tested meaningfully different root-cause strategies. Further work would require instrumenting compatibility-renderer arrange order rather than generating another evidence-free variation.
Selected Fix: PR's fix — it is the only implementation that passes all six supplied iOS scenarios. Neither alternative is demonstrably better; the PR's independent code-review concerns remain unresolved.


📝 Recommended PR Title & Description

Assessment: ✏️ Recommend updating — the current title lacks the required platform/component form, and the description is stale because the winning fix removes UIViewController.Title synchronization, narrows safe-area eligibility, and expands coverage from the stated 5/5 to 9 scenarios.

Recommended title

[iOS] NavigationPage: Fix non-Shell large-title collapse

Recommended description

Fixes #33037

## Summary

This completes the iOS 26 large-title fix for non-Shell `NavigationPage` scenarios.

### Root cause

For wrapped non-Shell content, the root layout consumed the top safe area before its scrollable child reached UIKit. The scrollable content therefore did not extend under the translucent navigation bar, preventing iOS 26 from consistently materializing the collapsed standard title.

### Fix

- In `src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs`, applies a handler-specific top `SafeAreaEdges` opt-out only when the page resolves to one visible, top-edge `ScrollView`, `ListView`, `ItemsView`, or `TableView` path under a visible translucent navigation bar with an effective large title.
- Preserves explicit app `SafeAreaEdges`, fixed siblings, top padding/margins, reserved `Grid` rows, hidden navigation bars, and pages whose effective large-title mode is inline.
- Re-evaluates dynamic content and layout eligibility without scanning recycled collection cells on the scroll path, and restores the override during page and renderer teardown.
- In `src/Core/src/Handlers/View/ViewHandler.iOS.cs`, invalidates safe-area caches for descendant MAUI visual-tree views when `SafeAreaEdges` changes.
- Limits the workaround to iOS 26; MacCatalyst is not enabled without dedicated coverage.

## Existing PR comparison

PR #33246 fixed the Shell path. This PR covers the remaining non-Shell `NavigationPage` path and adds regression coverage for the non-Shell scenarios that were still broken.

## What not to do

- Do not rely only on `SetContentScrollView`: it collapses the title but leaves wrapped scroll frames below the navigation bar, failing 3/6 visual scenarios.
- Do not hand off the frame only in `ScrollViewHandler.PlatformArrange`: that approach also failed the same 3 wrapped scenarios and introduced ambiguous safe-area edge ownership.
- Do not opt out the entire root layout merely because any descendant scrolls; fixed siblings can then render under the navigation bar.

## Verification

- Reproduced the broken non-Shell behavior on an iOS 26 Simulator.
- The supplied gate failed without the fix and passed all 6 original scenarios with the PR fix.
- Ran `BuildAndRunHostApp.ps1 -Platform ios -TestFilter "FullyQualifiedName~Issue33037NonShell"` against the reviewer-enhanced fix — 9/9 passed.
- Coverage includes direct `ScrollView`, `Grid` + `ScrollView`, `ContentView` + `Grid` + `ScrollView`, dynamically added wrapped content, `ListView`, and `CollectionView`, plus negative cases for a fixed header, a reserved `Grid` row, and a fixed sibling shown at runtime.
- Added iOS 26 visual baselines and gated the visual test to iOS 26.

@AlleSchonWeg please test the PR artifacts when they are available and let us know whether this resolves the remaining non-Shell scenarios for you.

🏁 Report — Final Recommendation

Comparative Report

Rank Candidate Regression result Assessment
1 pr-plus-reviewer PASS: 9/9 Preserves the PR's 6 passing scenarios, passes 3 new negative cases, and addresses the expert review's whole-root geometry, cell-recycling hot-path, hidden-bar, inherited-mode, cache-invalidation, baseline, and title-sync concerns.
2 pr PASS: 6/6 gate Only submitted candidate proven to fail without the fix and pass with it, but it retains 11 expert findings, including fixed-sibling overlap and collection-recycling work on the scroll path.
3 try-fix-1 FAIL: 3/6 Explicit UIKit content-scroll-view designation collapses all titles, but wrapped layouts still fail visual regression because their scroll frames remain below the navigation bar.
4 try-fix-2 FAIL: 3/6 Arrange-time top-edge frame handoff has no observable effect in the three wrapped cases and additionally risks treating top-edge ownership as ownership of every safe-area edge.

Comparison

try-fix-1 establishes that SetContentScrollView solves only UIKit's offset discovery; it does not make wrapped content extend under the bar. try-fix-2 attempts the missing geometry handoff but reproduces the same 3 wrapped snapshot failures. Both therefore rank below the two candidates that pass the regression matrix.

The raw PR is functionally effective, but its broad HasScrollableContent heuristic applies Top=None to non-scrolling siblings, and its removed-descendant filter fails after parent detachment. Its pre-iOS-26 baseline handling and several fixtures are also incomplete.

pr-plus-reviewer narrows eligibility to a single visible top-edge scroll path, handles runtime transitions, invalidates descendant safe-area caches through the MAUI visual tree, removes unrelated title synchronization, and adds discriminating negative coverage. It passes the original 6 scenarios plus all 3 added regressions.

Winner

pr-plus-reviewer

It is the only candidate that combines a passing positive regression matrix with passing coverage for the principal adjacent regressions identified during expert review. MacCatalyst remains intentionally excluded pending dedicated validation.


📱 UI Tests — CollectionView,Layout,ListView,Navigation,SafeAreaEdges,ScrollView,ViewBaseTests

Detected UI test categories: CollectionView,Layout,ListView,Navigation,SafeAreaEdges,ScrollView,ViewBaseTests

Deep UI tests — 750 passed, 1 failed across 6 categories on platform-pool agent (replaces in-process counts above). The deep UI run for 1 category (CollectionView) exceeded the per-category time budget (a very long-running category or a slow build/deploy) and was stopped before finishing. This is an infrastructure/timeout issue, not a code problem; re-run the review, and if a category consistently needs more time the per-category budget can be raised (DEEP_UITEST_CATEGORY_CAP_MIN / DEEP_UITEST_HARDSTOP_MIN). See the build-output.log in the drop-deep-uitests artifact.

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Layout 200/206 (1 ❌)
ListView 97/99 ✓
Navigation 116/119 ✓
SafeAreaEdges 67/67 ✓
ScrollView 158/159 ✓
ViewBaseTests 112/112 ✓
🔍 AI analysis of failures — PR-related vs unrelated

🔍 AI-generated triage (GitHub Copilot CLI) — a heuristic judgement of whether each deep UI test failure is connected to this PR's changes. Verify before relying on it.

Likely PR-related: one or more failures appear connected to this PR's changes.

  • ✗ PR-related — iOS large-title keyboard-scrolling layout (~1 test): EditorsScrollingPageTest exercises a non-scrolling ContentView page with iOS large titles, directly overlapping the PR's NavigationPage large-title safe-area and descendant-layout changes that could affect element visibility.
Layout — 1 failed test
EditorsScrollingPageTest
OpenQA.Selenium.NoSuchElementException : An element could not be located on the page using the given search parameters.; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
   at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.WebDriver.FindElement(String mechanism, String value)
   at OpenQA.Selenium.Appium.AppiumDriver.FindElement(String by, String value)
   at OpenQA.Selenium.Appium.MobileBy.FindElement(ISearchContext context)
   at OpenQA.Selenium.WebDriver.FindElement(By by)
   at OpenQA.Selenium.Appium.AppiumDriver.FindElement(By by)
   at Microsoft.Maui.TestCases.Tests.KeyboardScrolling.CloseiOSEditorKeyboard(AppiumDr
...

📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)


🧭 Next Steps — review latest findings

No alternative fix was selected for this run. Review the session findings and CI results before merging.

@kubaflo

kubaflo commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).

Mirror wrapped scroll offsets through a transparent direct-child UIScrollView on iOS 26 so UIKit can collapse NavigationPage large titles without changing app safe-area behavior. Cover fixed siblings, short content, dynamic content, CollectionView, and programmatic/restored offsets with simulator UI tests and visual baselines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b9546732-a28a-4b72-9928-aaeb2601f1d3
@kubaflo

kubaflo commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Final empirical validation — iOS 26.1 Simulator

Validated commit 1ac530ddd72 on iPhone 11 Pro / iOS 26.1 (23B86). The original failure was reproduced before the fix, then the final implementation was rebuilt, clean-launched, exercised through Appium, and recorded again.

Before: issue reproduced

The wrapped Grid/ScrollView scrolls, but the large title disappears instead of becoming a visible standard title.

ios26.1-grid-scrollview-large-title-disappears.mp4

Initial state:

Broken initial state

After scrolling — title missing:

Broken after scroll

After: final fix

The initial large title remains visible and collapses into the standard navigation title after the nested scroller moves.

ios26.1-grid-scrollview-large-title-collapses.mp4
Initial large title Collapsed standard title
Fixed Grid before scroll Fixed Grid after scroll

Regression evidence

Fixed sibling stays below the navigation bar:

Before scroll After scroll
Fixed header before scroll Fixed header after scroll

Hidden navigation bar preserves the status-bar safe area:

Hidden navigation bar safe area

A CollectionView.ScrollTo applied during OnAppearing is recognized immediately:

OnAppearing programmatic offset

Automated coverage

Test Run Successful.
Total tests: 11
Passed: 11

Command:

pwsh .github/scripts/BuildAndRunHostApp.ps1 \
  -Platform ios \
  -TestFilter "FullyQualifiedName~Issue33037NonShell" \
  -DeviceUdid "E6480069-9F54-4CB6-944D-7CB253586D5A"

Coverage includes direct/wrapped/dynamic ScrollView, ListView, Items2 CollectionView, fixed siblings, short nested content, explicit and OnAppearing programmatic offsets, initial-title rendering, and hidden-navigation-bar safe area. Final MAUI expert review found no remaining major or moderate correctness issues.

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

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

Suppressed comments (1)

src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33037NonShell.cs:26

  • scenarioName and fixedHeaderId are intentionally passed as null in several [TestCase] attributes, but the method signature declares them as non-nullable string. This makes the nullability contract misleading and can cause accidental null dereferences if the method body changes later.

Make these parameters nullable to reflect actual usage.

	public void LargeTitleCollapsesToVisibleStandardTitle(string buttonId, string scrollerId, string title, string targetItem, string scenarioName, string fixedHeaderId)

@kubaflo kubaflo 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.

Expert review — see inline comments.

Cache the resolved vertical scroller, skip hidden and horizontal candidates, and support wrapped TableView and WebView content with focused iOS regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b9546732-a28a-4b72-9928-aaeb2601f1d3
@kubaflo

kubaflo commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review feedback follow-up — empirically validated

Pushed b1da04e61f6 to address the three post-CI review findings:

  • caches the attached native scroller instead of rediscovering it on every layout;
  • skips hidden/transparent and horizontal candidates, selecting the top-edge vertical scroller;
  • supports wrapped TableView and WebView, resolving WKWebView.ScrollView explicitly.

Added simulator coverage for wrapped TableView, wrapped programmatically scrolled WebView, and a hidden + horizontal scroller preceding the visible vertical CollectionView.

Test Run Successful.
Total tests: 14
Passed: 14

Validated on a clean iPhone 11 Pro / iOS 26.1 (23B86) simulator with:

pwsh .github/scripts/BuildAndRunHostApp.ps1 \
  -Platform ios \
  -TestFilter "FullyQualifiedName~Issue33037NonShell" \
  -DeviceUdid "CB7862CA-0D12-426D-A7E2-C8F80723B32F"

The complete before/after video and screenshot evidence, including fixed-header, hidden-navigation-bar, and OnAppearing programmatic-scroll regression evidence, is preserved in the final empirical validation report.

A fresh MAUI expert review of the final diff found no major, moderate, or merge-blocking findings. All three review threads have been replied to and resolved.

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

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

Suppressed comments (2)

src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs:1692

  • NeedsLargeTitleProxyScrollView is gated by OperatingSystem.IsIOSVersionAtLeast(26) only. That API also returns true on MacCatalyst, so this proxy behavior will run on MacCatalyst 26+ even though the PR description/tests scope it to iOS 26 only. If this fix is intended to be iOS-only, explicitly exclude MacCatalyst here (or update the PR description/tests to cover MacCatalyst).
			bool NeedsLargeTitleProxyScrollView()
			{
				if (!OperatingSystem.IsIOSVersionAtLeast(26) || Child is null)
					return false;

src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs:1965

  • FindPlatformScrollView returns the first UIScrollView it encounters in a depth-first traversal, even if that scroll view is hidden/transparent. Since callers later reject ineligible scroll views (hidden/alpha), this can cause wrapped scrollables to be missed entirely when a hidden scroll view appears first in the subview tree. Prefer searching for the first eligible scroll view and skip hidden branches during traversal.
			static UIScrollView FindPlatformScrollView(UIView view)
			{
				var subviews = view.Subviews;
				for (int i = 0; i < subviews.Length; i++)
				{

@AlleSchonWeg

Copy link
Copy Markdown
Contributor

Hi @jfversluis ,
this changes are now merged. Do you have adressed my feedback in comment: #36511 (comment) ?
I'm not at work at the moment, so i can't test.

Thank you

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

Labels

area-controls-navigationpage platform/ios platform/macos macOS / Mac Catalyst s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) 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.

iOS Large Title display disappears

6 participants