Skip to content

[NET11][iOS/MacCatalyst] Replace NavigationRenderer with NavigationViewHandler - #36109

Merged
kubaflo merged 30 commits into
net11.0from
Net11.0-iOS-NavigationView-Handler
Aug 1, 2026
Merged

[NET11][iOS/MacCatalyst] Replace NavigationRenderer with NavigationViewHandler#36109
kubaflo merged 30 commits into
net11.0from
Net11.0-iOS-NavigationView-Handler

Conversation

@Tamilarasan-Paranthaman

@Tamilarasan-Paranthaman Tamilarasan-Paranthaman commented Jun 24, 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

Replaces the monolithic NavigationRenderer (~2700 lines, single file) with a layered NavigationViewHandler architecture for iOS and MacCatalyst. The handler is registered as the unconditional default — no feature flag or opt-in required.

Issues Fixed

Fixes #33081 (Note: The shared Core infrastructure is designed for potential reuse by a future Shell handler, but Shell integration is not part of this PR)

Motivation

The NavigationRenderer is a single class that IS a UINavigationController. It owns all navigation logic, bar appearance, toolbar management, FlyoutPage integration, and page lifecycle in one file. This makes it:

  • Hard to maintain — changes to toolbar logic risk breaking navigation timing
  • Impossible to share — Shell's navigation needs the same UINavigationController management but can't reuse anything from the renderer
  • Inconsistent with handler architecture — every other control has moved to handlers; NavigationPage was the last holdout on iOS

What Changed

New Files (Core layer — src/Core/)

File Purpose
Handlers/NavigationPage/NavigationViewHandler.iOS.cs Handler entry point: push/pop logic, VC map, stack sync, NavigationViewDelegate, ContainerViewController, NavigationViewHandlerControlsConfiguration
Platform/iOS/NavigationControllerManager/NavigationControllerManager.cs Shared UINavigationController manager: MauiNavigationController subclass, NavDelegate, GestureDelegate, push/pop TCS tracking
Platform/iOS/NavigationControllerManager/INavigationManagerDelegate.cs 8-method interface bridging Core ↔ consumer behavior

New Files (Controls layer — src/Controls/)

File Purpose
Platform/iOS/NavigationViewHandlerToolbarHelper.cs NavigationHandlerParentingViewController (per-page wrapper), TitleViewContainer, factory
NavigationPage/NavigationPage.Mapper.cs RemapForControls() — registers mappers + sets ControlsConfiguration
NavigationPage/NavigationPage.iOS.cs Mapper implementations: MapBarBackground, MapBarTextColor, MapHideNavigationBarSeparator, etc.

Modified Files

Category File Change
Handler Registration AppHostBuilderExtensions.cs AddHandler<NavigationPage, NavigationViewHandler>() for iOS/MacCatalyst
Virtual View NavigationPage.cs SendHandlerUpdateAsync() + MauiNavigationImpl + partial method declarations for deferred NavigatedTo (ShouldDeferNavigatedTo, FireDeferredNavigatedTo)
Virtual View (iOS) NavigationPage.iOS.cs iOS deferred NavigatedTo implementation + mapper implementations (MapBarBackground, MapBarTextColor, etc.)
Compatibility NavigationRenderer.cs Still implements INavigationViewHandler — available for manual fallback
Compatibility DisposeHelpers.cs Added DisconnectHandler() for root modal page cleanup (non-IDisposable handlers like NavigationViewHandler)
Device Tests NavigationPageTests.cs SetupBuilder(bool includeNavigationViewHandler) toggle for handler vs renderer tests
Device Tests (iOS) NavigationPageTests.iOS.cs iOS-specific handler tests (TabbedPage composition, lifecycle, etc.)

Architecture Overview

Handler Hierarchy

┌──────────────────────────────────────────────────────────┐
│                    CONTROLS LAYER                        │
│  NavigationPage.Mapper.cs       → mappers + config setup │
│  NavigationViewHandlerToolbarHelper.cs → per-page VC     │
│  NavigationPage.iOS.cs          → mapper implementations │
├──────────────────────────────────────────────────────────┤
│                     CORE LAYER                           │
│  NavigationViewHandler.iOS.cs   → push/pop/stack sync    │
│  NavigationControllerManager.cs → shared UINavController │
│  INavigationManagerDelegate.cs  → 8-method interface     │
└──────────────────────────────────────────────────────────┘

Key Design Decisions

Decision Rationale
Two-layer split (Core/Controls) Core owns UINavigationController management with no knowledge of Page, ToolbarItem, or XAML. Controls injects its behavior via a configuration record. This design is intended to allow a future iOS Shell handler to potentially reuse the same Core infrastructure.
NavigationViewHandlerControlsConfiguration record Sealed record with 5 required + 1 optional callback slots (NavigationBarType, CreateViewControllerForPage, OnNativePopCompleted, OnControllerAppeared, OnControllerDisappeared + optional OnMidStackChanged). Dependency inversion without circular assembly references.
Per-page wrapper VC (NavigationHandlerParentingViewController) Each page gets its own UIViewController that manages toolbar items, nav bar visibility, title view, back button, and large titles. Isolates per-page concerns from the navigation stack.
NavigationControllerManager as shared class Extracted UINavigationController lifecycle (push/pop TCS, gesture delegate, ShouldPopItem interception) into a reusable manager. Currently used by NavigationViewHandler; designed so a future iOS Shell handler could also consume it via INavigationManagerDelegate.
MauiNavigationController subclass Intercepts native back button via navigationBar:shouldPopItem: to support pop-blocking. Overrides ViewDidAppear/ViewDidDisappear/ViewDidLayoutSubviews to fire delegate callbacks for lifecycle.
Handler is unconditional default No feature flag. Registered in AppHostBuilderExtensions.cs. NavigationRenderer still exists for manual opt-out if needed.

Configuration Bridge

Core defines NavigationViewHandlerControlsConfiguration — a sealed record with callback slots:

Property Type Purpose
NavigationBarType required Type UINavigationBar subclass (e.g., MauiNavigationBar for MacCatalyst)
CreateViewControllerForPage required Func<IView, IMauiContext, UIViewController> Factory for per-page wrapper VC
OnNativePopCompleted required Action<IStackNavigationView, IView> Callback after UIKit-initiated pop completes
OnControllerAppeared required Action<IStackNavigationView> Callback on ViewDidAppear (loaded + Appearing)
OnControllerDisappeared required Action<IStackNavigationView> Callback on ViewDidDisappear (Disappearing)

Controls fills these at startup via NavigationPage.RemapForControls(). This avoids circular dependencies while giving the handler access to Controls-layer behavior (toolbar items, title views, lifecycle events).

INavigationManagerDelegate Interface (8 methods)

Method Purpose
GetNavigationBarVisibility(UIViewController) Returns (isHidden, animate) for nav bar per VC
ShouldPop() Allows blocking native back button / swipe-back
OnNavigationComplete(UINavigationController, UIViewController) Push/pop animation finished
OnWillShowViewController(...) Navigation controller will show a new VC
OnInteractivePopCompleted() Swipe-to-go-back gesture completed
OnNavigationControllerDidAppear() MauiNavigationController.ViewDidAppear fired
OnNavigationControllerDidDisappear() MauiNavigationController.ViewDidDisappear fired
OnViewDidLayoutSubviews(CGRect) Layout pass — propagates Frame to MAUI

Key Behaviors

Behavior Details
Secondary toolbar Uses UIMenu overflow button instead of bottom UIToolbar
Navigation bar Standard UINavigationBar (no custom subclass needed)
Lifecycle Appearing/Disappearing fire via MauiNavigationController.ViewDidAppear/ViewDidDisappear
Layout NavigationPage.Frame propagated via ViewDidLayoutSubviews → Arrange()
Loaded detection KVO trigger in ViewDidAppear for TabbedPage scenarios
Shared infra NavigationControllerManager designed to be reusable by a future Shell handler

Feature Parity

Full parity with NavigationRenderer including:

  • Push/pop/pop-to-root with animation
  • Insert/remove mid-stack pages
  • Bar background (color, brush, translucent)
  • Bar text color + per-page IconColor
  • Large titles (global + per-page)
  • Hide/show navigation bar per page
  • Back button title, a11y label, hide
  • Toolbar items (primary + secondary via UIMenu)
  • TitleView + TitleIcon
  • FlyoutPage hamburger button (works via existing static SetFlyoutLeftBarButton)
  • Orientation forwarding
  • Swipe-to-go-back with blocking support
  • Tab switch Appearing/Disappearing
  • iOS 26 Liquid Glass appearance

Shared Infrastructure Summary

Component Used By Purpose
NavigationControllerManager NavigationViewHandler (currently) UINavigationController push/pop lifecycle, TCS tracking, gesture delegate
MauiNavigationController Same Back button interception (ShouldPopItem), ViewDidAppear/ViewDidDisappear callbacks, ViewDidLayoutSubviews → Arrange
INavigationManagerDelegate Same 8-method interface bridging Core ↔ consumer
NavDelegate Same UINavigationControllerDelegate — resolves push/pop TCS, manages nav bar visibility
GestureDelegate Same Controls swipe-to-go-back gesture availability

Note: Shell's iOS handler has not been migrated yet. The shared infrastructure is designed with Shell reuse in mind, but actual integration will be validated during the Shell handler implementation. The INavigationManagerDelegate interface and NavigationControllerManager may need adjustments based on Shell's specific requirements.

Handler as Default

The handler is registered as the unconditional default in AppHostBuilderExtensions.cs — no feature flag or opt-in required.

What Changes By Default

  • NavigationPage uses NavigationViewHandler instead of NavigationRenderer on iOS/MacCatalyst
  • Secondary toolbar items appear in a UIMenu overflow button (instead of a bottom UIToolbar)
  • Appearing fires before PushViewController (not after ViewDidAppear)
  • New VCs are created per push (not reused from a cache)
  • NavigatedTo is deferred to ViewDidAppear via FireDeferredNavigatedTo (ensures NavigationProxy.Inner is wired)

How to Opt Out

If an app needs to fall back to the renderer, register it manually in MauiProgram.cs:

builder.ConfigureMauiHandlers(handlers =>
{
    handlers.AddHandler<NavigationPage, NavigationRenderer>();
});

NavigationRenderer remains in the Compatibility layer and continues to implement INavigationViewHandler.

Known Behavioral Differences

Aspect Renderer Handler
Appearing timing After ViewDidAppear (page visible) Before PushViewController (page not yet visible)
NavigatedTo timing After ViewDidAppear (renderer lifecycle) Deferred to ViewDidAppear via FireDeferredNavigatedTo
Page re-push Reuses existing VC Creates fresh VC (ensures clean Loaded state)

The handler's behavior now matches Android and Windows, the renderer was the outlier. These are cross-platform consistency improvements, not regressions.

Recommendation for apps: Use NavigatedTo instead of Appearing for post-navigation work that assumes the page is on-screen.

Testing

  • All existing NavigationPage device tests pass on the handler path
  • Renderer-specific tests continue to work via new NavigationPage(false, rootPage) constructor
  • New handler-path tests added for:
    • TabbedPage + NavigationPage composition (Loaded, Frame)
    • Tab switch Appearing/Disappearing
    • FlyoutPage hamburger icon
    • Back button blocking
    • Multi-page push/pop sequences

Migration Guidance for Custom Renderer Users

If you subclassed NavigationRenderer or accessed its internals, this section maps the old patterns to the new handler architecture.

Old → New Mapping

Renderer (Old) Handler (New) Notes
NavigationRenderer (IS a UINavigationController) NavigationViewHandler (HAS a UINavigationController via NavigationControllerManager) Handler is a ViewHandler<IStackNavigationView, UIView>, not a VC
Override ViewDidAppear Implement INavigationManagerDelegate.OnNavigationControllerDidAppear() Fires via MauiNavigationController.ViewDidAppear
Override ViewDidDisappear Implement INavigationManagerDelegate.OnNavigationControllerDidDisappear() Same pattern
Override ViewDidLayoutSubviews Implement INavigationManagerDelegate.OnViewDidLayoutSubviews(CGRect) Receives bounds directly
Access NavigationBar handler.NavigationController?.NavigationBar Via internal NavigationController property
Custom PopViewAsync override Provide OnNativePopCompleted callback in ControlsConfiguration Pop completion is callback-based, not override-based
Custom page VC creation Provide CreateViewControllerForPage in ControlsConfiguration Factory function instead of override

Adapter Pattern for Custom Renderers

If you must preserve custom renderer logic, wrap it using the handler registration:

// Keep using NavigationRenderer with your customizations
builder.ConfigureMauiHandlers(handlers =>
{
    handlers.AddHandler<NavigationPage, MyCustomNavigationRenderer>();
});

Toolbar Item Changes

Renderer Handler
Primary items → UIBarButtonItem in nav bar Same
Secondary items → bottom UIToolbar Secondary items → UIMenu overflow button in nav bar

Apps relying on the bottom toolbar for secondary items will see them move to a ··· overflow menu button in the navigation bar.

@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 -- 36109

Or

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

@sheiksyedm

Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@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 Jun 25, 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 — 3 findings

See inline comments for details.

Comment thread src/Core/src/Handlers/NavigationPage/NavigationViewHandler.iOS.cs
Comment thread src/Controls/src/Core/NavigationPage/NavigationPage.iOS.cs Outdated
@MauiBot MauiBot added 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 Jun 25, 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 Jun 25, 2026
@Tamilarasan-Paranthaman
Tamilarasan-Paranthaman force-pushed the Net11.0-iOS-NavigationView-Handler branch from dd93616 to 6e68418 Compare June 29, 2026 14:42
@sheiksyedm

Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@sheiksyedm

Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@kubaflo

This comment has been minimized.

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 28 out of 28 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/Core/src/Platform/iOS/NavigationControllerManager/NavigationControllerManager.cs:468

  • GestureDelegate.ShouldBegin always returns true when the stack depth > 1, so consumers cannot block the swipe-back gesture via INavigationManagerDelegate.ShouldPop() (e.g., Shell uses ShouldPop() to veto back navigation). Since this manager is intended to be reusable, the interactive pop gesture should consult the delegate before allowing the gesture to begin.
                // Always allow the gesture — matches the renderer, which returns true
                // and lets UIKit drive the interactive transition. After the gesture
                // completes, OnInteractivePopCompleted syncs the MAUI stack.
                return true;

@vishnumenon2684

Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines

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

@kubaflo

This comment has been minimized.

@MauiBot

This comment has been minimized.

@PureWeen PureWeen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten-head adversarial review — see the new inline finding.

@PureWeen PureWeen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten-head adversarial review — one new issue

  • ❌ The cached top-controller status-bar delegation is never initialized by the root SetViewControllers path, so the initial page's PrefersStatusBarHidden preference is ignored until a later push/pop updates the cache. Repo specialist + 1/3 reviewers.

Prior review status

The rewritten head addresses many earlier findings. Existing current-head threads already cover forced non-animated completion bookkeeping and the pending-controller insertion implementation, so they were not duplicated.

Test coverage

No changed test verifies PrefersStatusBarHidden on the initial/root page before any navigation, including an initially off-screen tab.

Methodology: 3 independent reviewers with adversarial consensus + repo domain specialist. Code review only; CI status was not evaluated.

@kubaflo

This comment has been minimized.

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

AI Review Summary

@Tamilarasan-Paranthaman — new AI review results are available based on this last commit: 099b747.

Gate Failed Confidence Low Platform iOS


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

Gate Result: ❌ FAILED

Platform: IOS · Base: net11.0 · Merge base: c6dc61fa

🩺 Fix does not pass the tests — every test still fails after applying the fix. The PR's change does not resolve the failure(s).

Test Without Fix (expect FAIL) With Fix (expect PASS)
📱 CollectionViewTests CollectionViewTests ✅ FAIL — 545s ❌ FAIL — 1337s
📱 FlyoutPageTests FlyoutPageTests ✅ FAIL — 347s ❌ FAIL — 1299s
🔴 Without fix — 📱 CollectionViewTests: FAIL ✅ · 545s

(no coded error found; showing last 1200 chars)

   "exitCode": 1,
        "exitCodeName": "TESTS_FAILED",
        "platform": "apple",
        "device": "iPhone 11 Pro",
        "deviceOsVersion": "26.5",
        "files": [
          {
            "name": "test-ios-simulator-64_26.5-D3D27171-493F-4A63-B004-D804E1835AAB.log",
            "type": "executionlog"
          },
          {
            "name": "list-ios-simulator-64_26.5-20260729_164334.log",
            "type": "devicelist"
          },
          {
            "name": "test-ios-simulator-64_26.5-20260729_164341.log",
            "type": "testlog"
          },
          {
            "name": "iPhone 11 Pro.log",
            "type": "systemlog"
          },
          {
            "name": "Microsoft.Maui.Controls.DeviceTests.log",
            "type": "systemlog"
          },
          {
            "name": "com.microsoft.maui.controls.devicetests.log",
            "type": "applicationlog"
          },
          {
            "name": "xunit-test-ios-simulator-64_26.5-20260729_164341.xml",
            "type": "xmllog"
          }
        ]
      }
      <<XHARNESS_RESULT_END>>
XHarness exit code: 1 (TESTS_FAILED)
  Passed: 0
  Failed: 0
  Tests completed with exit code: 1
🟢 With fix — 📱 CollectionViewTests: FAIL ❌ · 1337s

(no coded error found; showing last 1200 chars)

xitCode": 1,
        "exitCodeName": "TESTS_FAILED",
        "platform": "apple",
        "device": "iPhone 11 Pro",
        "deviceOsVersion": "26.5",
        "files": [
          {
            "name": "test-ios-simulator-64_26.5-D3D27171-493F-4A63-B004-D804E1835AAB.log",
            "type": "executionlog"
          },
          {
            "name": "list-ios-simulator-64_26.5-20260729_165612.log",
            "type": "devicelist"
          },
          {
            "name": "test-ios-simulator-64_26.5-20260729_165615.log",
            "type": "testlog"
          },
          {
            "name": "iPhone 11 Pro.log",
            "type": "systemlog"
          },
          {
            "name": "Microsoft.Maui.Controls.DeviceTests.log",
            "type": "systemlog"
          },
          {
            "name": "com.microsoft.maui.controls.devicetests.log",
            "type": "applicationlog"
          },
          {
            "name": "xunit-test-ios-simulator-64_26.5-20260729_165615.xml",
            "type": "xmllog"
          }
        ]
      }
      <<XHARNESS_RESULT_END>>
XHarness exit code: 1 (TESTS_FAILED)
  Passed: 1787
  Failed: 214
  Tests completed with exit code: 1
🔴 Without fix — 📱 FlyoutPageTests: FAIL ✅ · 347s

(no coded error found; showing last 1200 chars)

exitCode": 1,
        "exitCodeName": "TESTS_FAILED",
        "platform": "apple",
        "device": "iPhone 11 Pro",
        "deviceOsVersion": "26.5",
        "files": [
          {
            "name": "test-ios-simulator-64_26.5-D3D27171-493F-4A63-B004-D804E1835AAB.log",
            "type": "executionlog"
          },
          {
            "name": "list-ios-simulator-64_26.5-20260729_164951.log",
            "type": "devicelist"
          },
          {
            "name": "test-ios-simulator-64_26.5-20260729_164955.log",
            "type": "testlog"
          },
          {
            "name": "iPhone 11 Pro.log",
            "type": "systemlog"
          },
          {
            "name": "Microsoft.Maui.Controls.DeviceTests.log",
            "type": "systemlog"
          },
          {
            "name": "com.microsoft.maui.controls.devicetests.log",
            "type": "applicationlog"
          },
          {
            "name": "xunit-test-ios-simulator-64_26.5-20260729_164955.xml",
            "type": "xmllog"
          }
        ]
      }
      <<XHARNESS_RESULT_END>>
XHarness exit code: 1 (TESTS_FAILED)
  Passed: 846
  Failed: 202
  Tests completed with exit code: 1
🟢 With fix — 📱 FlyoutPageTests: FAIL ❌ · 1299s

(no coded error found; showing last 1200 chars)

xitCode": 1,
        "exitCodeName": "TESTS_FAILED",
        "platform": "apple",
        "device": "iPhone 11 Pro",
        "deviceOsVersion": "26.5",
        "files": [
          {
            "name": "test-ios-simulator-64_26.5-D3D27171-493F-4A63-B004-D804E1835AAB.log",
            "type": "executionlog"
          },
          {
            "name": "list-ios-simulator-64_26.5-20260729_171758.log",
            "type": "devicelist"
          },
          {
            "name": "test-ios-simulator-64_26.5-20260729_171802.log",
            "type": "testlog"
          },
          {
            "name": "iPhone 11 Pro.log",
            "type": "systemlog"
          },
          {
            "name": "Microsoft.Maui.Controls.DeviceTests.log",
            "type": "systemlog"
          },
          {
            "name": "com.microsoft.maui.controls.devicetests.log",
            "type": "applicationlog"
          },
          {
            "name": "xunit-test-ios-simulator-64_26.5-20260729_171802.xml",
            "type": "xmllog"
          }
        ]
      }
      <<XHARNESS_RESULT_END>>
XHarness exit code: 1 (TESTS_FAILED)
  Passed: 4610
  Failed: 250
  Tests completed with exit code: 1

⚠️ Failure Details

  • CollectionViewTests FAILED with fix (should pass)
    • Device tests: 214 of 2001 failed
  • FlyoutPageTests FAILED with fix (should pass)
    • Device tests: 250 of 4860 failed
📁 Fix files reverted (11 files)
  • src/Controls/src/Core/Button/Button.iOS.cs
  • src/Controls/src/Core/Compatibility/Handlers/iOS/DisposeHelpers.cs
  • src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs
  • src/Controls/src/Core/NavigationPage/NavigationPage.Legacy.cs
  • src/Controls/src/Core/NavigationPage/NavigationPage.Mapper.cs
  • src/Controls/src/Core/NavigationPage/NavigationPage.cs
  • src/Controls/src/Core/NavigationPage/NavigationPage.iOS.cs
  • src/Controls/src/Core/VisualElement/VisualElement.cs
  • src/Core/src/Handlers/NavigationPage/NavigationViewHandler.iOS.cs
  • src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
  • src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt

New files (not reverted):

  • src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cs
  • src/Core/src/Platform/iOS/NavigationControllerManager/INavigationManagerDelegate.cs
  • src/Core/src/Platform/iOS/NavigationControllerManager/NavigationControllerManager.cs

📱 UI Tests — Button,Navigation,ViewBaseTests

Detected UI test categories: Button,Navigation,ViewBaseTests

Deep UI tests — 262 passed, 31 failed across 3 categories on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Button 71/72 ✓
Navigation 79/113 (31 ❌)
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 NavigationPage handler/lifecycle failures (~10 tests): the PR replaces the iOS NavigationPage renderer with NavigationViewHandler and rewires navigation lifecycle callbacks, matching failures around push/pop ordering, back-button callbacks, swipe-back crash, and OnNavigatedTo behavior.
  • ✗ PR-related — iOS navigation bar/background/translucency coverage (~15 tests): the repeated Issue17022 failures exercise NavigationPage/FlyoutPage transparent and translucent navigation scenarios, while the diff heavily changes iOS navigation bar background, translucency, title, status-bar, and toolbar mapping.
  • ✗ PR-related — Navigation category timeout cluster (~6 tests): the remaining Bugzilla/navigation tests fail with the same “timed out waiting for element” pattern inside Navigation UI tests on iOS, and the PR changes shared and iOS-specific navigation infrastructure compiled into this run.

Strongest signal: all failures are in the iOS Navigation deep run, and this PR directly changes iOS NavigationPage handler registration plus navigation lifecycle/bar behavior rather than an unrelated platform or broad infrastructure area.

Navigation — 31 failed tests
Issue17022Test("NewFlyoutPageTransparentButton",False)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Bugzilla31255Test
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.UtilExtensions.AssertMemoryTest(IApp app) in /_/src/Controls/tests/TestCases.Shared.Tests/UtilExtensions.cs:line 135
   at Microsoft.Maui.TestCases.Tests.Issues.Bugzilla31255.Bugzilla31255Test() in /_/src/Controls/tests/TestCases.Shared.Tests/Test
...
Issue17022Test("NewNavigationPageGridButton",False)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue31366PushingWithModalStackCausesIncorrectStackOrder
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Bugzilla31366.Issue31366PushingWithModalStackCausesIncorrectStackOrder() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla31366.cs:line 31
   at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target
...
Issue17022Test("SemiTransparentFlyoutPageBackgroundColor",True,True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
MakingFragmentRelatedChangesWhileAppIsBackgroundedFails
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue23399.MakingFragmentRelatedChangesWhileAppIsBackgroundedFails() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23399.cs:line 18
   at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** argumen
...
Bugzilla32615Test
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Bugzilla32615.Bugzilla32615Test() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla32615.cs:line 21
   at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack
...
Issue17022Test("NewNavigationPageButton",False)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleO
...
Issue17022Test("SemiTransparentNavigationPageBackgroundColor",True,True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("NewNavigationPageGridTransparentTranslucentButton",True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("NewNavigationPageGridTranslucentButton",False,True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("SemiTransparentFlyoutPageBrush",True,True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
SwipeBackNavCrashTestsSwipeBackDoesNotCrash
The app was expected to be running still, investigate as possible crash
TearDown : The app was expected to be running still, investigate as possible crash
at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45
   at InvokeStub_UITestBase.TestTearDown(Object, Object, IntPtr*)

--TearDown
   at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45
   at InvokeStub_UITestBase.TestTearDown(Object, Object, IntPtr*)

1)    at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45
   at InvokeS
...
OnBackButtonPressedReturnFalseShouldNavigateBack
OnBackButtonPressed should have been called even when returning false.
Assert.That(statusText, Is.EqualTo("OnBackButtonPressed Called And Returned False"))
  Expected string length 45 but was 7. Strings differ at index 0.
  Expected: "OnBackButtonPressed Called And Returned False"
  But was:  "Waiting"
  -----------^
at Microsoft.Maui.TestCases.Tests.Issues.Issue8296.OnBackButtonPressedReturnFalseShouldNavigateBack() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue8296.cs:line 73

1)    at Microsoft.Maui.TestCases.Tests.Issues.Issue8296.OnBackButtonPressedReturnFalseShouldNavigateBack() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue8296.cs:line 73
OnBackButtonPressedShouldBeInvokedOnIOSWithNavigationPage
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue8296.OnBackButtonPressedShouldBeInvokedOnIOSWithNavigationPage() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue8296.cs:line 43
   at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** argumen
...
Issue17022Test("NewFlyoutPageGridTransparentButton",True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
OnNavigatedToShouldTrigger
The app was expected to be running still, investigate as possible crash
TearDown : The app was expected to be running still, investigate as possible crash
at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45

--TearDown
   at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45

1)    at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45
Issue17022Test("NewFlyoutPageButton",False)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("NewFlyoutPageTranslucentButton",False)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Bugzilla30166Test
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Bugzilla30166.Bugzilla30166Test() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla30166.cs:line 21
   at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack
...
Issue17022Test("NewFlyoutPageGridButton",False)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("NewNavigationPageTransparentTranslucentButton",False)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("NewFlyoutPageGridTranslucentButton",False,True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("NewNavigationPageGridTransparentButton",True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("NewFlyoutPageTransparentTranslucentButton",False)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("NewFlyoutPageGridTransparentTranslucentButton",True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Issue17022Test("NewNavigationPageTransparentButton",False)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...
Bugzilla29453Test
The app was expected to be running still, investigate as possible crash
TearDown : The app was expected to be running still, investigate as possible crash
at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45

--TearDown
   at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45

1)    at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45
PoppedOnlyFiresOnce
The app was expected to be running still, investigate as possible crash
TearDown : The app was expected to be running still, investigate as possible crash
at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45

--TearDown
   at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45

1)    at UITest.Appium.NUnit.UITestBase.UITestBaseTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 159
   at UITest.Appium.NUnit.UITestBase.TestTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 45
Issue17022Test("SemiTransparentNavigationPageBrush",True,True)
System.TimeoutException : Timed out waiting for element...
at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2761
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2788
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 797
   at Microsoft.Maui.TestCases.Tests.Issues.Issue17022.Issue17022Test(String testButtonID, Boolean isTopOfScreen, Boolean requiresScreenshot) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17022.cs:line 44
   at InvokeStub_Issue17022.Issue17022Test(Object, Span`1)

...

(+1 more — see TRX in artifact)

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


📋 Pre-Flight — Context & Validation

Issue: #33081 - Replace NavigationRenderer with NavigationViewHandler
PR: #36109 - [NET11][iOS/MacCatalyst] Replace NavigationRenderer with NavigationViewHandler
Platforms Affected: iOS, MacCatalyst
Files Changed: 14 implementation/API, 14 test

Key Findings

  • PR replaces the default iOS/MacCatalyst NavigationPage implementation with an unconditional NavigationViewHandler, adding shared NavigationControllerManager infrastructure and Controls-layer toolbar/navigation callbacks.
  • Gate verification already failed on iOS device tests; do not rerun the gate phase. The failed selectors were CollectionViewTests and FlyoutPageTests, both expected to fail without the fix and pass with it, but both still failed with the PR fix.
  • This is broad NavigationPage handler/platform plumbing with startup, lifecycle, toolbar, FlyoutPage, Modal, TabbedPage, CollectionView, and status-bar blast radius.
  • Code review found multiple concrete renderer-parity regressions that should guide alternate candidates: native back-button cancellation, initial status-bar delegation, interactive swipe completion timing, MacCatalyst titlebar refresh, back-title fallback, and iPad FlyoutPage rotation/resize toolbar state.

Code Review Summary

Verdict: NEEDS_CHANGES
Confidence: low
Errors: 6 | Warnings: 0 | Suggestions: 0

Key code review findings:

  • src/Core/src/Handlers/NavigationPage/NavigationViewHandler.iOS.cs:443 — native iOS back-button cancellation is bypassed because ShouldPop() returns true unconditionally instead of honoring SendBackButtonPressed().
  • src/Core/src/Handlers/NavigationPage/NavigationViewHandler.iOS.cs:105 / src/Core/src/Platform/iOS/NavigationControllerManager/NavigationControllerManager.cs:217 — initial root status-bar hidden preference is not delegated because _currentTopVC is not initialized for the SetViewControllers root path.
  • src/Core/src/Platform/iOS/NavigationControllerManager/NavigationControllerManager.cs:426 — interactive swipe-back updates MAUI before UIKit finishes the transition.
  • src/Core/src/Platform/iOS/NavigationControllerManager/NavigationControllerManager.cs:351 — MacCatalyst MauiNavigationBar.RefreshIfNeeded() equivalent is missing after navigation.
  • src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cs:617BackButtonAccessibilityLabel-only path can lose the visible fallback title.
  • src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cs:230 — iPad FlyoutPage left button can go stale after rotation/resize.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #36109 Replace NavigationRenderer with unconditional iOS/MacCatalyst NavigationViewHandler plus new navigation manager and Controls toolbar bridge ❌ FAILED (Gate) 28 files Original PR did not satisfy iOS CollectionViewTests and FlyoutPageTests gate selectors

🔬 Code Review — Deep Analysis

Code Review — PR #36109

Independent Assessment

What this changes: Replaces the default iOS/MacCatalyst NavigationRenderer path with NavigationViewHandler, adding a shared NavigationControllerManager, Controls-layer wrapper VCs, toolbar/title/back-button mapping, and lifecycle/device-test updates.

Inferred motivation: Move NavigationPage to handler architecture and create reusable iOS navigation infrastructure.

Reconciliation with PR Narrative

Author claims: Full parity with NavigationRenderer, including back-button blocking, FlyoutPage hamburger behavior, MacCatalyst titlebar support, lifecycle, and iOS 26 appearance.

Agreement/disagreement: The architecture matches the stated goal, but several parity claims do not match the code. Native back-button cancellation, MacCatalyst nav-bar refresh, iPad FlyoutPage rotation handling, and some initial status-bar delegation paths remain regressed.

Prior Review Reconciliation

Prior ❌ Error Finding Source Status Evidence
Non-animated push/pop can hang waiting for DidShowViewController MauiBot ✅ Fixed Current code calls CompleteNonAnimatedNavigation() after non-animated push/pop.
Navigation proxy Inner dropped when swapping to MauiNavigationImpl PureWeen ✅ Fixed NavigationPage.cs:126-134 preserves oldInner.
Final toolbar native items leak on dispose PureWeen ✅ Fixed NavigationViewHandlerToolbarHelper.cs:287-305 disposes final right/toolbar items.
BackButtonAccessibilityLabel-only can blank visible back text MauiBot ❌ Unresolved NavigationViewHandlerToolbarHelper.cs:625 uses only child.Title, not child.Title ?? navPage.Title.
MacCatalyst MauiNavigationBar.RefreshIfNeeded() missing after navigation MauiBot ❌ Unresolved DidShowViewController ends at NavigationControllerManager.cs:384 with no refresh call.
Interactive swipe-back syncs before UIKit finishes transition MauiBot ❌ Unresolved NavigationControllerManager.cs:426-429 calls OnInteractivePopCompleted() from NotifyWhenInteractionChanges.
iPad FlyoutPage toolbar state not recomputed on rotation/resize MauiBot ❌ Unresolved NavigationViewHandlerToolbarHelper.cs:230-241 updates only title view, not left button.
Initial root status-bar delegation not initialized PureWeen ❌ Unresolved Root path uses SetViewControllers at NavigationViewHandler.iOS.cs:107; _currentTopVC is only set by push/DidShow (NavigationControllerManager.cs:210-223).

Blast Radius Assessment

  • Runs for all instances: Yes — iOS/MacCatalyst NavigationPage is now registered unconditionally.
  • Startup impact: Yes — affects initial NavigationPage creation, root VC installation, lifecycle, loaded detection, and status-bar delegation.
  • Static/shared state: Yes — NavigationViewHandler.ControlsConfiguration is static and controls all handler instances.

CI Status

  • Required-check result: gh pr checks --required unavailable because gh is unauthenticated.
  • Public check-runs fallback: red — Build Analysis, maui-pr, and multiple maui-pr-uitests legs are failing/cancelled on head 099b747.
  • Classification: undetermined from available public status summary.
  • Action taken: used public GitHub status fallback; confidence capped low.

Findings

❌ Error — Native iOS back-button cancellation is bypassed

NavigationViewHandler.iOS.cs:443-449 returns true unconditionally from ShouldPop(). The legacy renderer called CurrentPage.SendBackButtonPressed() and returned false when the page handled/cancelled back navigation. A page overriding OnBackButtonPressed() to show a confirmation dialog and return true will still be popped by the native back button.

❌ Error — Initial root page status-bar hidden preference is not delegated

The initial stack path installs the root VC with SetViewControllers (NavigationViewHandler.iOS.cs:105-108) but never calls UpdateCurrentTopVC. ChildViewControllerForStatusBarHidden() only delegates when _currentTopVC is set (NavigationControllerManager.cs:217-223), so the root page’s iOS status-bar hidden preference is ignored until a later push/pop updates the cache.

❌ Error — Interactive swipe-back updates MAUI before UIKit finishes

NavigationControllerManager.cs:426-429 calls OnInteractivePopCompleted() as soon as the interaction commits, before final DidShowViewController. That immediately reaches stack sync/disposal in NavigationViewHandler.iOS.cs:520-527, so popped VCs/pages can be removed while UIKit is still completing the transition.

❌ Error — MacCatalyst titlebar nav-bar refresh is dropped

The legacy renderer and Shell call (navigationController.NavigationBar as MauiNavigationBar)?.RefreshIfNeeded() from DidShowViewController. The new DidShowViewController path (NavigationControllerManager.cs:351-384) does not, so MacCatalyst custom-titlebar/safe-area adjustments can remain stale after navigation.

❌ Error — Back accessibility-label-only path can lose visible fallback title

When only BackButtonAccessibilityLabel is set, NavigationViewHandlerToolbarHelper.cs:617-628 creates a custom back item with Title = child.Title. If the visible title comes from NavigationPage.Title fallback and child.Title is null, this suppresses UIKit’s default visible back text.

❌ Error — iPad FlyoutPage left button can go stale after rotation/resize

The legacy renderer recomputed UpdateLeftBarButtonItem() on every iPad ViewWillTransitionToSize. The new handler only updates the title view for iOS/MacCatalyst 26+ (NavigationViewHandlerToolbarHelper.cs:230-241), so FlyoutPage detail NavigationPages can keep stale hamburger/back-button state after rotation or Stage Manager resizing.

Failure-Mode Probing

  • Back button handled by page: native ShouldPopItem delegates to ShouldPop(), which always returns true; cancellation is ignored.
  • Root NavigationPage with status-bar hidden set before first navigation: root VC is installed without initializing _currentTopVC; status delegation falls back to base controller.
  • Interactive swipe-back commit: MAUI stack sync/disposal runs at interaction-change time, before DidShowViewController.
  • Back accessibility label with parent title fallback: custom BackBarButtonItem title becomes null, blanking visible text.
  • MacCatalyst custom titlebar after push/pop: TitleBarNeedsRefresh has no DidShow refresh path in the new manager.
  • iPad FlyoutPage rotation: layout-dependent ShouldShowToolbarButton() is not recomputed.

External Output Contract

Consumer token/pattern Producer location Producer emission condition Consumer assumption Ordinary negative case Downstream effect
N/A N/A No changed code classifies external tool output with regex/string literal. N/A N/A N/A

Verdict: NEEDS_CHANGES

Confidence: low
Summary: This PR changes default iOS/MacCatalyst navigation plumbing with broad startup and handler blast radius. Multiple concrete renderer-parity regressions remain unresolved, and CI is red/undetermined, so it should not merge as-is.


🛠️ Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix-1 Back-button + root-VC + swipe-pop compatibility shim in the new handler path ❌ FAIL 3 files Fixed several parity gaps, but iOS device selectors still failed; learned that FlyoutPage/NavigationViewHandler co-existence is a deeper architectural issue
2 try-fix-2 Fix UIKit child view-controller containment lifecycle in PhoneFlyoutPageRenderer ✅ PASS (PR-specific failures resolved) 1 file Reduced failures to 6 unrelated pre-existing HybridWebView failures; all Navigation/Flyout/Modal/Toolbar regressions from gate passed
PR PR #36109 Replace NavigationRenderer with unconditional iOS/MacCatalyst NavigationViewHandler plus new navigation manager and Controls toolbar bridge ❌ FAILED (Gate) 28 files Original PR did not satisfy iOS CollectionViewTests and FlyoutPageTests gate selectors

Candidate Details

try-fix-1 — ❌ FAIL

Approach: Kept the new NavigationViewHandler architecture but added a small compatibility shim for three renderer parity gaps: back-button cancellation through SendBackButtonPressed(), initial root VC status-bar delegation, and deferred interactive swipe-pop stack sync until UIKit completion.

Test Results: CollectionViewTests and FlyoutPageTests both ran on iOS and failed. Attempt output reported CollectionViewTests: 7433 passed, 286 failed and FlyoutPageTests: 8374 passed, 298 failed.

Failure Analysis: The shim did not address the dominant failure mode. The useful learning is that the PR’s failures are not only individual NavigationPage parity gaps; PhoneFlyoutPageRenderer is still renderer-based and appears incompatible with a handler-managed UINavigationController child. Future candidates should focus on FlyoutPage/NavigationPage co-existence rather than isolated navigation callbacks.

try-fix-2 — ✅ PASS (selected)

Approach: Fixed the UIKit containment protocol in the existing iOS PhoneFlyoutPageRenderer: after AddChildViewController, add the child view and call DidMoveToParentViewController(parent); before removal, call WillMoveToParentViewController(null), remove the view, then RemoveFromParentViewController(). This addresses the renderer/handler co-existence problem at the compatibility container layer instead of changing Core navigation callbacks.

Diff Summary: One file changed: src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs (+41 / -8). The changes cover EmptyContainers, PackContainers, and UpdateFlyoutPageContainers.

Test Results: The iOS CollectionViewTests and FlyoutPageTests selectors both ran. Final runs reported 962 run / 941 passed / 6 failed / 15 ignored for each selector. The remaining 6 failures were identical HybridWebView InvokeJavaScriptMethodThatThrows{String,Number,Error} tests, unrelated to Navigation/Flyout/Modal/Toolbar. Attempt analysis records that all PR-specific failing Navigation/Flyout/Modal/Toolbar tests passed after this fix.

Failure Analysis: No PR-specific failure remained. The residual XHarness exit-code-1 comes from pre-existing HybridWebView failures outside the PR blast radius.

Why better than the PR fix: It is a targeted one-file fix at the actual compatibility boundary. It makes the existing FlyoutPage renderer comply with UIKit containment rules so both legacy NavigationRenderer and the new handler-managed UINavigationController receive appearance/lifecycle forwarding correctly. This avoids broad Core-layer churn and directly addresses the failures observed in gate and try-fix-1.

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 1 Yes Focus next attempt on explicit compatibility between PhoneFlyoutPageRenderer and handler-managed NavigationPage controllers, or avoid mixing renderer FlyoutPage with the new handler path.
claude-opus-4.7 1 Yes Implemented the FlyoutPage compatibility hypothesis via proper UIKit child view-controller containment lifecycle.

Exhausted: No — stopped because try-fix-2 passed the PR-specific regression surface and is demonstrably better than the PR's current fix.
Selected Fix: Candidate #2PhoneFlyoutPageRenderer UIKit containment lifecycle fix. It is smaller, correctly layered, and resolves the Navigation/Flyout/Modal/Toolbar failures left by PR #36109.


📝 Recommended PR Title & Description

Assessment: ✏️ Recommend updating — the current title and strong description accurately describe the renderer-to-handler migration, but they omit the winning PhoneFlyoutPageRenderer UIKit containment fix required for the PR-specific iOS regressions to pass.

Recommended title

[NET11][iOS/MacCatalyst] NavigationPage: Replace renderer with handler and fix Flyout containment

Recommended description

### Description of Change

Replaces the monolithic `NavigationRenderer` (~2700 lines, single file) with a layered `NavigationViewHandler` architecture for iOS and MacCatalyst. The handler is registered as the unconditional default — no feature flag or opt-in required.

This also fixes the legacy `PhoneFlyoutPageRenderer` UIKit child view-controller containment lifecycle so it can correctly host handler-managed `UINavigationController` children. The FlyoutPage compatibility renderer now completes the standard containment handshake:

- Add: `AddChildViewController` → add the child view → `DidMoveToParentViewController(parent)`
- Remove: `WillMoveToParentViewController(null)` → remove the child view → `RemoveFromParentViewController()`

### Issues Fixed

Fixes #33081

Note: The shared Core infrastructure is designed for potential reuse by a future Shell handler, but Shell integration is not part of this PR.

### Motivation

The `NavigationRenderer` is a single class that is a `UINavigationController`. It owns all navigation logic, bar appearance, toolbar management, FlyoutPage integration, and page lifecycle in one file. This makes it:

- Hard to maintain — changes to toolbar logic risk breaking navigation timing
- Impossible to share — Shell's navigation needs the same `UINavigationController` management but cannot reuse anything from the renderer
- Inconsistent with handler architecture — every other control has moved to handlers; NavigationPage was the last holdout on iOS

### Root Cause / Key Insight

The new `NavigationViewHandler` manages a standalone `UINavigationController` through `NavigationControllerManager`. When that controller is hosted inside the legacy iOS `PhoneFlyoutPageRenderer`, UIKit appearance and lifecycle forwarding depends on correct child view-controller containment.

`PhoneFlyoutPageRenderer` previously added and removed child view controllers without completing the required `DidMoveToParentViewController(...)` and `WillMoveToParentViewController(null)` calls. The legacy `NavigationRenderer` tolerated this because it was itself a `UINavigationController` subclass, but the handler-managed navigation controller depends on those containment callbacks for appearance, toolbar, loaded/unloaded, modal, and leak-sensitive cleanup behavior.

### What Changed

#### New Files (Core layer — `src/Core/`)

| File | Purpose |
|------|---------|
| `Handlers/NavigationPage/NavigationViewHandler.iOS.cs` | Handler entry point: push/pop logic, VC map, stack sync, `NavigationViewDelegate`, `ContainerViewController`, `NavigationViewHandlerControlsConfiguration` |
| `Platform/iOS/NavigationControllerManager/NavigationControllerManager.cs` | Shared `UINavigationController` manager: `MauiNavigationController` subclass, `NavDelegate`, `GestureDelegate`, push/pop TCS tracking |
| `Platform/iOS/NavigationControllerManager/INavigationManagerDelegate.cs` | Interface bridging Core and consumer behavior |

#### New Files (Controls layer — `src/Controls/`)

| File | Purpose |
|------|---------|
| `Platform/iOS/NavigationViewHandlerToolbarHelper.cs` | `NavigationHandlerParentingViewController` per-page wrapper, `TitleViewContainer`, toolbar/title/back-button integration |
| `NavigationPage/NavigationPage.Mapper.cs` | `RemapForControls()` registers Controls-specific mappers and handler configuration |
| `NavigationPage/NavigationPage.iOS.cs` | iOS deferred `NavigatedTo` implementation and mapper implementations such as `MapBarBackground`, `MapBarTextColor`, and separator visibility |

#### Modified Files

| Category | File | Change |
|----------|------|--------|
| Handler Registration | `AppHostBuilderExtensions.cs` | Registers `NavigationViewHandler` for `NavigationPage` on iOS/MacCatalyst |
| Virtual View | `NavigationPage.cs` | Adds `SendHandlerUpdateAsync()`, `MauiNavigationImpl`, and partial methods for deferred `NavigatedTo` |
| Virtual View (iOS) | `NavigationPage.iOS.cs` | Implements iOS deferred `NavigatedTo` and mapper behavior |
| Compatibility | `NavigationRenderer.cs` / legacy NavigationPage files | Keeps renderer compatibility available for manual fallback paths |
| Compatibility | `DisposeHelpers.cs` | Adds handler disconnect support for root modal page cleanup with non-`IDisposable` handlers |
| Compatibility | `PhoneFlyoutPageRenderer.cs` | Completes UIKit child view-controller containment when adding/removing flyout/detail child controllers so handler-managed navigation controllers receive appearance/lifecycle forwarding |
| Device Tests | `NavigationPageTests.cs` | Adds `SetupBuilder(bool includeNavigationViewHandler)` toggle for handler-vs-renderer coverage |
| Device Tests (iOS) | `NavigationPageTests.iOS.cs` and related iOS device tests | Adds/updates handler coverage for NavigationPage, FlyoutPage, Modal, Toolbar, TabbedPage, lifecycle, and memory-sensitive scenarios |

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

- Do not try to fix the FlyoutPage failures only by adding more callbacks inside `NavigationViewHandler` or `NavigationControllerManager`; the dominant failure is the legacy `PhoneFlyoutPageRenderer` containment boundary.
- Do not assume a child `UINavigationController` will receive appearance callbacks if the container skips `DidMoveToParentViewController(...)` after add or `WillMoveToParentViewController(null)` before removal.
- Do not treat remaining HybridWebView `InvokeJavaScriptMethodThatThrows{String,Number,Error}` failures as evidence against the navigation fix; those failures are unrelated to NavigationPage/FlyoutPage/Toolbar behavior.

### Platforms Tested

- [x] iOS
- [ ] MacCatalyst
- [ ] Android
- [ ] Windows

🏁 Report — Final Recommendation

Comparative Report — PR #36109 Candidates

Ranking

Rank Candidate Regression result Scope Assessment
1 try-fix-2 ✅ PASS for PR-specific regressions; only unrelated HybridWebView failures remained 1 file Best candidate. It fixes the actual compatibility boundary by completing UIKit containment in PhoneFlyoutPageRenderer, resolving all observed Navigation/Flyout/Modal/Toolbar failures from the gate.
2 pr-plus-reviewer Not independently run as a combined candidate PR + reviewer fixes Likely better than the raw PR because it includes the containment fix and other parity corrections, but it is broader and unverified compared with the proven one-file candidate.
3 try-fix-1 ❌ FAIL (CollectionViewTests: 286 failures, FlyoutPageTests: 298 failures) 3 files Correctly identified several renderer-parity gaps, but did not address the dominant FlyoutPage/NavigationViewHandler coexistence failure.
4 pr ❌ FAIL gate (CollectionViewTests: 214 failures, FlyoutPageTests: 250 failures) 28 files Architectural migration is valuable, but the submitted fix leaves PR-specific iOS regressions and multiple expert-review parity issues.

Candidate analysis

pr

The raw PR replaces the iOS/MacCatalyst NavigationRenderer with an unconditional NavigationViewHandler, adding new Core navigation-manager infrastructure and Controls toolbar/lifecycle integration. It failed the iOS gate: CollectionViewTests and FlyoutPageTests both still failed after applying the PR fix. Expert review also found unresolved parity issues around legacy FlyoutPage hosting, native back-button cancellation, root status-bar delegation, back-title fallback, and MacCatalyst titlebar refresh.

pr-plus-reviewer

This candidate is the PR plus the expert reviewer's actionable feedback. Its most important addition is the PhoneFlyoutPageRenderer UIKit containment fix proven by try-fix-2; it would also address additional parity gaps called out by the reviewer. It ranks above pr and try-fix-1 because it targets the root compatibility boundary, but below try-fix-2 because STEP 5a did not run this larger combined patch and the extra changes increase blast radius without regression proof.

try-fix-1

This candidate patched native back-button cancellation, root status-bar delegation, and interactive swipe-pop timing. Those are real parity gaps, but the test result was worse than the raw PR for the gate selectors (CollectionViewTests: 286 failures; FlyoutPageTests: 298 failures). Its failure analysis correctly identified the deeper issue: PhoneFlyoutPageRenderer and the new handler-managed UINavigationController were incompatible without a compatibility-layer containment fix.

try-fix-2

This candidate changed only src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs. It completed the UIKit view-controller containment protocol in PackContainers, UpdateFlyoutPageContainers, and EmptyContainers: add child controller before adding its view, call DidMoveToParentViewController(...) after insertion, and call WillMoveToParentViewController(null) before removal.

The result was materially passing for this PR's regression surface. Both iOS selector runs ended with only 6 failures, all identical pre-existing HybridWebView InvokeJavaScriptMethodThatThrows{String,Number,Error} tests unrelated to Navigation/Flyout/Modal/Toolbar. The previously failing NavigationPage, FlyoutPage, Modal, Toolbar, TabbedPage, and leak scenarios passed.

Winner

Winner: try-fix-2

It is the only candidate with empirical evidence that the PR-specific regression surface is fixed. It is also the smallest and best-layered fix: instead of adding more Core navigation complexity, it repairs the legacy Controls Compatibility container that hosts the new handler-managed navigation controller.


🧭 Next Steps — alternative fix proposed (try-fix-2)

Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against automatically generated candidates and selected try-fix-2 as the strongest fix.

Why: try-fix-2 is the only candidate that passed the PR-specific iOS regression surface: all Navigation/Flyout/Modal/Toolbar failures cleared, leaving only unrelated pre-existing HybridWebView failures. It is also the smallest, best-layered fix because it repairs UIKit containment in PhoneFlyoutPageRenderer at the compatibility boundary.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (try-fix-2)
diff --git a/src/Controls/src/Core/Button/Button.iOS.cs b/src/Controls/src/Core/Button/Button.iOS.cs
index 61aacab892..d1341a27f7 100644
--- a/src/Controls/src/Core/Button/Button.iOS.cs
+++ b/src/Controls/src/Core/Button/Button.iOS.cs
@@ -159,10 +159,12 @@ namespace Microsoft.Maui.Controls
 		{
 			bounds = this.ComputeFrame(bounds);
 
-			var platformButton = Handler?.PlatformView as UIButton;
-
-			// Layout the image and title of the button
-			LayoutButton(platformButton, this, bounds);
+			// During animated transitions, UIKit may trigger LayoutSubviews after the handler
+			// has been disconnected. Guard against accessing a null PlatformView.
+			if (Handler?.PlatformView is UIButton platformButton)
+			{
+				LayoutButton(platformButton, this, bounds);
+			}
 
 			return new Size(bounds.Width, bounds.Height);
 		}
diff --git a/src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs
index 41de494f92..7d595d2241 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs
@@ -328,11 +328,23 @@ namespace Microsoft.Maui.Controls.Handlers.Compatibility
 
 		void EmptyContainers()
 		{
-			foreach (var child in _detailController.View.Subviews.Concat(_flyoutController.View.Subviews))
-				child.RemoveFromSuperview();
-
-			foreach (var vc in _detailController.ChildViewControllers.Concat(_flyoutController.ChildViewControllers))
+			// Proper UIKit containment removal: WillMoveToParentViewController(null) must
+			// be called BEFORE the view is removed so the child VC gets a chance to run
+			// disappearance/cleanup logic; RemoveFromParentViewController is called AFTER.
+			// The legacy NavigationRenderer (a UINavigationController subclass) tolerated the
+			// old incomplete lifecycle, but NavigationViewHandler-managed UINavigationControllers
+			// require this to reliably fire ViewWillDisappear/ViewDidDisappear on their pages
+			// (which drives page-unloaded eventing and toolbar cleanup on Detail swaps).
+			foreach (var vc in _detailController.ChildViewControllers.Concat(_flyoutController.ChildViewControllers).ToArray())
+			{
+				vc.WillMoveToParentViewController(null);
+				vc.View?.RemoveFromSuperview();
 				vc.RemoveFromParentViewController();
+			}
+
+			// Remove any orphan subviews that were not owned by a child VC.
+			foreach (var child in _detailController.View.Subviews.Concat(_flyoutController.View.Subviews).ToArray())
+				child.RemoveFromSuperview();
 		}
 
 		void HandleFlyoutPropertyChanged(object sender, PropertyChangedEventArgs e)
@@ -556,21 +568,34 @@ namespace Microsoft.Maui.Controls.Handlers.Compatibility
 		{
 			_detailController.View.BackgroundColor = new UIColor(1, 1, 1, 1);
 
+			// Proper UIKit containment for the two container VCs:
+			// AddChildViewController → AddSubview → DidMoveToParentViewController.
+			// AddChildViewController auto-calls WillMoveToParentViewController(parent);
+			// we still owe the child DidMoveToParentViewController(parent) after the subview
+			// is in the hierarchy. Without it, appearance callbacks aren't reliably forwarded
+			// to grandchildren, which breaks NavigationHandlerParentingViewController's
+			// ViewWillAppear (toolbar/left-bar-button refresh) when hosted here.
 			if (!FlyoutOverlapsDetailsInPopoverMode)
 			{
+				AddChildViewController(_flyoutController);
+				AddChildViewController(_detailController);
+
 				View.AddSubview(_flyoutController.View);
 				View.AddSubview(_detailController.View);
 
-				AddChildViewController(_flyoutController);
-				AddChildViewController(_detailController);
+				_flyoutController.DidMoveToParentViewController(this);
+				_detailController.DidMoveToParentViewController(this);
 			}
 			else
 			{
+				AddChildViewController(_detailController);
+				AddChildViewController(_flyoutController);
+
 				View.AddSubview(_detailController.View);
 				View.AddSubview(_flyoutController.View);
 
-				AddChildViewController(_detailController);
-				AddChildViewController(_flyoutController);
+				_detailController.DidMoveToParentViewController(this);
+				_flyoutController.DidMoveToParentViewController(this);
 			}
 		}
 
@@ -614,15 +639,22 @@ namespace Microsoft.Maui.Controls.Handlers.Compatibility
 
 			((FlyoutPage)Element).Flyout.PropertyChanged += HandleFlyoutPropertyChanged;
 
+			// Proper UIKit containment for the flyout/detail renderer VCs:
+			// AddChildViewController → AddSubview → DidMoveToParentViewController.
+			// AddChildViewController implicitly calls WillMoveToParentViewController(parent);
+			// DidMoveToParentViewController must be called explicitly by the container.
+			// Missing DidMoveToParentViewController breaks appearance forwarding through the
+			// hosted UINavigationController (which the new NavigationViewHandler depends on
+			// for toolbar/left-bar-button refresh and page loaded/unloaded eventing).
 			UIView flyoutView = flyoutRenderer.ViewController.View;
-
-			_flyoutController.View.AddSubview(flyoutView);
 			_flyoutController.AddChildViewController(flyoutRenderer.ViewController);
+			_flyoutController.View.AddSubview(flyoutView);
+			flyoutRenderer.ViewController.DidMoveToParentViewController(_flyoutController);
 
 			UIView detailView = detailRenderer.ViewController.View;
-
-			_detailController.View.AddSubview(detailView);
 			_detailController.AddChildViewController(detailRenderer.ViewController);
+			_detailController.View.AddSubview(detailView);
+			detailRenderer.ViewController.DidMoveToParentViewController(_detailController);
 
 			SetNeedsStatusBarAppearanceUpdate();
 			if (OperatingSystem.IsIOSVersionAtLeast(11))
diff --git a/src/Controls/src/Core/Compatibility/Handlers/iOS/DisposeHelpers.cs b/src/Controls/src/Core/Compatibility/Handlers/iOS/DisposeHelpers.cs
index 157f8475b1..39b755485b 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/iOS/DisposeHelpers.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/iOS/DisposeHelpers.cs
@@ -12,9 +12,11 @@ namespace Microsoft.Maui.Controls.Handlers.Compatibility
 			{
 				if (child is VisualElement ve)
 				{
-					ve.Handler?.DisconnectHandler();
+					// Capture handler before DisconnectHandler() — it nulls VirtualView.Handler.
+					var handler = ve.Handler;
+					handler?.DisconnectHandler();
 
-					if (ve.Handler is IDisposable disposable)
+					if (handler is IDisposable disposable)
 						disposable.Dispose();
 				}
 			}
@@ -32,8 +34,14 @@ namespace Microsoft.Maui.Controls.Handlers.Compatibility
 
 					renderer.PlatformView?.RemoveFromSuperview();
 
-					if (view.Handler is IDisposable disposable)
+					// Capture handler before DisconnectHandler() — it nulls VirtualView.Handler.
+					var handler = visualElement.Handler;
+					handler?.DisconnectHandler();
+
+					if (handler is IDisposable disposable)
+					{
 						disposable.Dispose();
+					}
 				}
 			}
 		}
diff --git a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs
index 5faa88bf74..94afc97e6c 100644
--- a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs
+++ b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs
@@ -96,7 +96,7 @@ public static partial class AppHostBuilderExtensions
 			handlersCollection.AddHandler<SearchBar, SearchBarHandler2>();
 			handlersCollection.AddHandler<Slider, SliderHandler2>();
 			handlersCollection.AddHandler<DatePicker, DatePickerHandler2>();
-            handlersCollection.AddHandler<Entry, EntryHandler2>();
+			handlersCollection.AddHandler<Entry, EntryHandler2>();
 		}
 		else
 		{
@@ -112,7 +112,7 @@ public static partial class AppHostBuilderExtensions
 			handlersCollection.AddHandler<SearchBar, SearchBarHandler>();
 			handlersCollection.AddHandler<Slider, SliderHandler>();
 			handlersCollection.AddHandler<DatePicker, DatePickerHandler>();
-            handlersCollection.AddHandler<Entry, EntryHandler>();
+			handlersCollection.AddHandler<Entry, EntryHandler>();
 		}
 #else
 		handlersCollection.AddHandler<Label, LabelHandler>();
@@ -203,7 +203,7 @@ public static partial class AppHostBuilderExtensions
 #endif
 
 #if IOS || MACCATALYST
-		handlersCollection.AddHandler(typeof(NavigationPage), typeof(Handlers.Compatibility.NavigationRenderer));
+		handlersCollection.AddHandler<NavigationPage, NavigationViewHandler>();
 		handlersCollection.AddHandler<TabbedPage, TabbedViewHandler>();
 		handlersCollection.AddHandler(typeof(FlyoutPage), typeof(Handlers.Compatibility.PhoneFlyoutPageRenderer));
 #endif
@@ -335,6 +335,11 @@ public static partial class AppHostBuilderExtensions
 		ImageButton.RemapForControls();
 
 		Slider.RemapForControls();
+
+#if IOS || MACCATALYST
+		NavigationPage.RemapForControls();
+#endif
+
 		return builder;
 	}
 }
diff --git a/src/Controls/src/Core/NavigationPage/NavigationPage.Legacy.cs b/src/Controls/src/Core/NavigationPage/NavigationPage.Legacy.cs
index 1644a77fe0..111d8f84e7 100644
--- a/src/Controls/src/Core/NavigationPage/NavigationPage.Legacy.cs
+++ b/src/Controls/src/Core/NavigationPage/NavigationPage.Legacy.cs
@@ -187,7 +187,7 @@ namespace Microsoft.Maui.Controls
 
 			var previousPage = CurrentPage;
 			var navigationType = DetermineNavigationType();
-			
+
 			SendNavigating(navigationType, previousPage);
 			FireDisappearing(CurrentPage);
 			FireAppearing(page);
@@ -203,13 +203,13 @@ namespace Microsoft.Maui.Controls
 
 				if (args.Task != null)
 					await args.Task;
-			} 
-			
+			}
+
 			SendNavigated(previousPage, navigationType);
 			Pushed?.Invoke(this, args);
 		}
 
-#if IOS
+#if IOS || MACCATALYST
 		// Because iOS currently doesn't use our `IStackNavigationView` structures
 		// there are scenarios where the legacy handler needs to alert the xplat
 		// code of when a navigation has occurred.
diff --git a/src/Controls/src/Core/NavigationPage/NavigationPage.Mapper.cs b/src/Controls/src/Core/NavigationPage/NavigationPage.Mapper.cs
index a3929d683e..e122c06670 100644
--- a/src/Controls/src/Core/NavigationPage/NavigationPage.Mapper.cs
+++ b/src/Controls/src/Core/NavigationPage/NavigationPage.Mapper.cs
@@ -1,6 +1,10 @@
 #nullable disable
 using System;
 using Microsoft.Maui.Controls.Compatibility;
+using Microsoft.Maui.Handlers;
+#if IOS || MACCATALYST
+using Microsoft.Maui.Controls.Handlers.Compatibility;
+#endif
 
 namespace Microsoft.Maui.Controls
 {
@@ -9,8 +13,84 @@ namespace Microsoft.Maui.Controls
 		internal static new void RemapForControls()
 		{
 			// Adjust the mappings to preserve Controls.NavigationPage legacy behaviors
-#if IOS
+#if IOS || MACCATALYST
 			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(PlatformConfiguration.iOSSpecific.NavigationPage.PrefersLargeTitlesProperty.PropertyName, MapPrefersLargeTitles);
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(Page.TitleProperty.PropertyName, MapTitle);
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(NavigationPage.BarBackgroundColorProperty.PropertyName, MapBarBackground);
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(NavigationPage.BarBackgroundProperty.PropertyName, MapBarBackground);
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(NavigationPage.BarTextColorProperty.PropertyName, MapBarTextColor);
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(PlatformConfiguration.iOSSpecific.NavigationPage.HideNavigationBarSeparatorProperty.PropertyName, MapHideNavigationBarSeparator);
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(PlatformConfiguration.iOSSpecific.NavigationPage.StatusBarTextColorModeProperty.PropertyName, MapStatusBarTextColorMode);
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(PlatformConfiguration.iOSSpecific.Page.PrefersHomeIndicatorAutoHiddenProperty.PropertyName, MapPrefersHomeIndicatorAutoHidden);
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(PlatformConfiguration.iOSSpecific.Page.PrefersStatusBarHiddenProperty.PropertyName, MapPrefersStatusBarHidden);
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(PlatformConfiguration.iOSSpecific.Page.PreferredStatusBarUpdateAnimationProperty.PropertyName, MapPreferredStatusBarUpdateAnimation);
+
+#pragma warning disable CS0618 // Type or member is obsolete
+			NavigationViewHandler.Mapper.ReplaceMapping<NavigationPage, NavigationViewHandler>(PlatformConfiguration.iOSSpecific.NavigationPage.IsNavigationBarTranslucentProperty.PropertyName, MapIsNavigationBarTranslucent);
+#pragma warning restore CS0618 // Type or member is obsolete
+
+			// Wire all Controls-layer integration in one place.
+			// This connects the Core-layer NavigationViewHandler to Controls-layer
+			// NavigationPage features (toolbar, lifecycle, nav bar type).
+			NavigationViewHandler.ControlsConfiguration = new NavigationViewHandlerControlsConfiguration
+			{
+				NavigationBarType = typeof(Handlers.Compatibility.MauiNavigationBar),
+				CreateViewControllerForPage = NavigationViewHandlerToolbarHelper.CreateViewControllerForPage,
+				OnNativePopCompleted = (navigationView, poppedPage) =>
+				{
+					if (navigationView is NavigationPage navPage && poppedPage is Page page)
+					{
+						// Match renderer's RemoveAsyncInner — fire lifecycle events
+						// that NavigationFinished (stack sync) does not handle.
+						navPage.FireDisappearing(page);
+
+						// Fire NavigatedFrom on the popped page directly, bypassing
+						// SendNavigatedFromHandler's HasNavigatedTo guard which blocks
+						// subsequent pages in a multi-pop scenario.
+						page.SendNavigatedFrom(new NavigatedFromEventArgs(navPage.CurrentPage, NavigationType.Pop));
+
+						// Fire NavigatedTo + Appearing on CurrentPage only if not already done
+						// (avoids duplicate events for multi-pop where this callback fires per page).
+						if (!navPage.CurrentPage.HasNavigatedTo)
+						{
+							navPage.FireAppearing(navPage.CurrentPage);
+							navPage.CurrentPage.SendNavigatedTo(new NavigatedToEventArgs(page, NavigationType.Pop));
+						}
+
+						navPage.Popped?.Invoke(navPage, new NavigationEventArgs(page));
+					}
+				},
+				OnControllerAppeared = (navigationView) =>
+				{
+					if (navigationView is VisualElement ve)
+					{
+						ve.RefreshPlatformLoadedStatus();
+					}
+					(navigationView as Page)?.SendAppearing();
+
+					// Fire deferred NavigatedTo if it was skipped in OnHandlerChangedCore
+					// because NavigationProxy.Inner wasn't wired yet at handler init time.
+					// By ViewDidAppear, the Window has parented the page and Inner is set.
+					// Also set status bar style when the nav controller appears (ViewDidAppear),
+					// matching renderer's ViewWillAppear -> SetStatusBarStyle() pattern.
+					if (navigationView is NavigationPage navPage)
+					{
+						navPage.FireDeferredNavigatedTo();
+						SetStatusBarStyle(navPage);
+					}
+				},
+				OnControllerDisappeared = (navigationView) =>
+				{
+					(navigationView as Page)?.SendDisappearing();
+				},
+				OnMidStackChanged = (topVC) =>
+				{
+					if (topVC is NavigationHandlerParentingViewController parentingVC)
+					{
+						parentingVC.NotifyStackChanged();
+					}
+				}
+			};
 #endif
 		}
 	}
diff --git a/src/Controls/src/Core/NavigationPage/NavigationPage.cs b/src/Controls/src/Core/NavigationPage/NavigationPage.cs
index 21f4a39de4..29bcd59c78 100644
--- a/src/Controls/src/Core/NavigationPage/NavigationPage.cs
+++ b/src/Controls/src/Core/NavigationPage/NavigationPage.cs
@@ -60,7 +60,25 @@ namespace Microsoft.Maui.Controls
 
 		partial void Init();
 
+		// Deferred NavigatedTo support (iOS/MacCatalyst only):
+		// On iOS, the handler connects (OnHandlerChangedCore) before the Window parents
+		// the page, so NavigationProxy.Inner is null at that point. If NavigatedTo fires
+		// immediately, any PushModalAsync called from a NavigatedTo handler will silently
+		// fail (NavigationProxy queues the request and returns Task.CompletedTask).
+		// With the renderer, OnHandlerChangedCore was skipped (IsShimmed()=true) and
+		// NavigatedTo fired later from the renderer's ViewDidAppear.
+		// These partial methods let iOS defer SendNavigated to OnControllerAppeared
+		// (ViewDidAppear), when Inner is wired. On Android/Windows these are no-ops
+		// because Inner is already set before the handler connects.
+		partial void ShouldDeferNavigatedTo(ref bool defer);
+		partial void FireDeferredNavigatedTo();
+		partial void OnHandlerDisconnected();
+
 #if IOS || MACCATALYST
+		// On iOS/MacCatalyst, default to legacy NavigationImpl (event-based).
+		// UseHandlerNavigation() is called when NavigationViewHandler connects,
+		// enabling MauiNavigationImpl (RequestNavigation-based).
+		// This ensures the renderer fallback works without any special handling.
 		const bool UseMauiHandler = false;
 #else
 		const bool UseMauiHandler = true;
@@ -95,6 +113,28 @@ namespace Microsoft.Maui.Controls
 				PushPage(root);
 		}
 
+		/// <summary>
+		/// Switches from legacy NavigationImpl to MauiNavigationImpl.
+		/// Called when NavigationViewHandler connects on iOS/MacCatalyst.
+		/// </summary>
+		internal void UseHandlerNavigation()
+		{
+			if (!_setForMaui)
+			{
+				_setForMaui = true;
+
+				// Preserve the old proxy's Inner — it was wired by OnParentSet
+				// when the Window connected. Without this, the new proxy starts
+				// with Inner=null and modal navigation silently fails.
+				var oldInner = NavigationProxy?.Inner;
+				Navigation = new MauiNavigationImpl(this);
+				if (oldInner is not null)
+				{
+					NavigationProxy.Inner = oldInner;
+				}
+			}
+		}
+
 		/// <summary>Gets or sets the background color for the bar at the top of the NavigationPage. This is a bindable property.</summary>
 		public Color BarBackgroundColor
 		{
@@ -742,6 +782,19 @@ namespace Microsoft.Maui.Controls
 		{
 			base.OnHandlerChangedCore();
 
+#if IOS || MACCATALYST
+			// On iOS/MacCatalyst, enable handler-based navigation (MauiNavigationImpl)
+			// when NavigationViewHandler connects. Constructor defaults to legacy
+			// NavigationImpl on these platforms to support renderer fallback.
+			if (Handler is NavigationViewHandler && !_setForMaui)
+			{
+				UseHandlerNavigation();
+				// The legacy NavigationImpl may have set CurrentNavigationTask (e.g. PushAsync
+				// in a subclass constructor). Clear it so SendHandlerUpdateAsync can take over.
+				CurrentNavigationTask = null;
+			}
+#endif
+
 			if (Navigation is MauiNavigationImpl && InternalChildren.Count > 0)
 			{
 				var navStack = Navigation.NavigationStack;
@@ -751,12 +804,18 @@ namespace Microsoft.Maui.Controls
 
 				var navigationType = DetermineNavigationType();
 
+				// On iOS, ShouldDeferNavigatedTo sets defer=true when Inner is null.
+				// When deferred, SendNavigated is skipped here and fired later from
+				// OnControllerAppeared (ViewDidAppear) via FireDeferredNavigatedTo.
+				bool deferNavigatedTo = false;
+				ShouldDeferNavigatedTo(ref deferNavigatedTo);
+
 				SendHandlerUpdateAsync(false, null,
 				() =>
 				{
 					FireAppearing(CurrentPage);
 				},
-				() =>
+				deferNavigatedTo ? null : () =>
 				{
 					SendNavigated(null, navigationType);
 				})
@@ -765,10 +824,15 @@ namespace Microsoft.Maui.Controls
 
 			// If the handler is disconnected and we're still waiting for updates from the handler
 			// Just complete any waits
-			if (Handler == null && _waitingCount > 0)
+			if (Handler is null && _waitingCount > 0)
 			{
 				((IStackNavigation)this).NavigationFinished(this.NavigationStack);
 			}
+
+			if (Handler is null)
+			{
+				OnHandlerDisconnected();
+			}
 		}
 
 		NavigationType DetermineNavigationType()
diff --git a/src/Controls/src/Core/NavigationPage/NavigationPage.iOS.cs b/src/Controls/src/Core/NavigationPage/NavigationPage.iOS.cs
index 2cb76ec92d..ac3184ff70 100644
--- a/src/Controls/src/Core/NavigationPage/NavigationPage.iOS.cs
+++ b/src/Controls/src/Core/NavigationPage/NavigationPage.iOS.cs
@@ -1,17 +1,541 @@
 #nullable disable
+using System;
+using Microsoft.Maui.Controls.Platform;
 using UIKit;
+using iOSSpecificNavigationPage = Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.NavigationPage;
 
 namespace Microsoft.Maui.Controls
 {
 	public partial class NavigationPage
 	{
+		GradientBrush _currentBarBackgroundBrush;
+		NavigationType? _deferredNavigationType;
+		Page _deferredCurrentPage;
+
+		/// <summary>
+		/// Cleans up iOS-specific subscriptions and resources when the handler disconnects.
+		/// Matches the renderer's Dispose cleanup pattern.
+		/// </summary>
+		partial void OnHandlerDisconnected()
+		{
+			if (_currentBarBackgroundBrush is GradientBrush gb)
+			{
+				gb.InvalidateGradientBrushRequested -= OnBarBackgroundBrushInvalidated;
+				gb.Parent = null;
+			}
+
+			_currentBarBackgroundBrush = null;
+			_deferredNavigationType = null;
+			_deferredCurrentPage = null;
+		}
+
+		/// <summary>
+		/// On iOS, the handler connects before the Window parents the page,
+		/// so NavigationProxy.Inner is null during OnHandlerChangedCore.
+		/// If Inner is null, defer SendNavigated (NavigatedTo) to ViewDidAppear
+		/// when navigation infrastructure is fully wired.
+		/// See NavigationPage.cs partial method declarations for full explanation.
+		/// </summary>
+		partial void ShouldDeferNavigatedTo(ref bool defer)
+		{
+			if (((Internals.NavigationProxy)Navigation).Inner is null)
+			{
+				defer = true;
+				_deferredNavigationType = DetermineNavigationType();
+				_deferredCurrentPage = CurrentPage;
+			}
+		}
+
+		/// <summary>
+		/// Fires the deferred SendNavigated that was skipped in OnHandlerChangedCore.
+		/// Called from OnControllerAppeared (ViewDidAppear) in NavigationPage.Mapper.cs.
+		/// </summary>
+		partial void FireDeferredNavigatedTo()
+		{
+			if (_deferredNavigationType is NavigationType navType)
+			{
+				var page = _deferredCurrentPage;
+				_deferredNavigationType = null;
+				_deferredCurrentPage = null;
+
+				// Use the captured page, not CurrentPage — CurrentPage may have
+				// changed if navigation happened before ViewDidAppear fired.
+				if (page is not null)
+				{
+					page.SendNavigatedTo(new NavigatedToEventArgs(null, navType));
+				}
+			}
+		}
+
 		public static void MapPrefersLargeTitles(NavigationViewHandler handler, NavigationPage navigationPage) =>
 			MapPrefersLargeTitles((INavigationViewHandler)handler, navigationPage);
 
 		public static void MapPrefersLargeTitles(INavigationViewHandler handler, NavigationPage navigationPage)
 		{
 			if (handler is IPlatformViewHandler nvh && nvh.ViewController is UINavigationController navigationController)
-				Platform.NavigationPageExtensions.UpdatePrefersLargeTitles(navigationController, navigationPage);
+			{
+				NavigationPageExtensions.UpdatePrefersLargeTitles(navigationController, navigationPage);
+			}
+		}
+
+		/// <summary>
+		/// When NavigationPage.Title changes, refresh the current top VC's
+		/// NavigationItem.Title if the child page's own Title is null (R7-4).
+		/// </summary>
+		static void MapTitle(NavigationViewHandler handler, NavigationPage navigationPage)
+		{
+			if (handler is IPlatformViewHandler nvh &&
+				nvh.ViewController is UINavigationController navController &&
+				navController.TopViewController is NavigationHandlerParentingViewController topVC)
+			{
+				topVC.RefreshTitleFromNavigationPage();
+			}
+		}
+
+		static void MapBarBackground(NavigationViewHandler handler, NavigationPage navigationPage)
+		{
+			var navBar = handler.NavigationController?.NavigationBar;
+
+			if (navBar is null)
+			{
+				return;
+			}
+
+			var barBackgroundColor = navigationPage.BarBackgroundColor;
+			var barBackground = navigationPage.BarBackground;
+
+			// Manage GradientBrush subscription — matches renderer pattern
+			if (navigationPage._currentBarBackgroundBrush is GradientBrush oldGradientBrush)
+			{
+				oldGradientBrush.Parent = null;
+				oldGradientBrush.InvalidateGradientBrushRequested -= navigationPage.OnBarBackgroundBrushInvalidated;
+			}
+
+			navigationPage._currentBarBackgroundBrush = barBackground as GradientBrush;
+
+			if (navigationPage._currentBarBackgroundBrush is GradientBrush newGradientBrush)
+			{
+				newGradientBrush.Parent = navigationPage;
+				newGradientBrush.InvalidateGradientBrushRequested += navigationPage.OnBarBackgroundBrushInvalidated;
+			}
+
+			if (barBackground is SolidColorBrush scb)
+			{
+				barBackgroundColor = scb.Color;
+				barBackground = null;
+			}
+
+#pragma warning disable CS0618 // Type or member is obsolete
+			bool isTranslucentExplicitlySet = navigationPage.IsSet(iOSSpecificNavigationPage.IsNavigationBarTranslucentProperty);
+			bool userTranslucentValue = isTranslucentExplicitlySet && iOSSpecificNavigationPage.GetIsNavigationBarTranslucent(navigationPage);
+#pragma warning restore CS0618 // Type or member is obsolete
+
+			var navigationBarAppearance = navBar.StandardAppearance;
+
+			if (barBackgroundColor is null && barBackground is null)
+			{
+				navigationBarAppearance.ConfigureWithOpaqueBackground();
+				navigationBarAppearance.BackgroundColor = ColorExtensions.BackgroundColor;
+				// Match renderer: default translucency is driven by IsNavigationBarTranslucent (defaults to false)
+				navBar.Translucent = userTranslucentValue;
+
+				SetupDefaultNavigationBarAppearance(navBar, navigationBarAppearance);
+			}
+			else if (barBackgroundColor is null && barBackground is not null)
+			{
+				// Gradient/image brush with no explicit color — reset appearance
+				// to clear any stale BackgroundColor/Translucent from a previous call.
+				navigationBarAppearance.ConfigureWithOpaqueBackground();
+				navigationBarAppearance.BackgroundColor = null;
+				navBar.Translucent = userTranslucentValue;
+			}
+			else if (barBackgroundColor is not null)
+			{
+				// Match renderer: if IsNavigationBarTranslucent is explicitly set, respect it;
+				// otherwise base translucency on the background color alpha
+				if (isTranslucentExplicitlySet)
+				{
+					if (userTranslucentValue)
+					{
+						navigationBarAppearance.ConfigureWithTransparentBackground();
+						navBar.Translucent = true;
+					}
+					else
+					{
+						navigationBarAppearance.ConfigureWithOpaqueBackground();
+						navBar.Translucent = false;
+					}
+				}
+				else
+				{
+					if (barBackgroundColor.Alpha < 1f)
+					{
+						navigationBarAppearance.ConfigureWithTransparentBackground();
+						navBar.Translucent = true;
+					}
+					else
+					{
+						navigationBarAppearance.ConfigureWithOpaqueBackground();
+						navBar.Translucent = false;
+					}
+				}
+
+				navigationBarAppearance.BackgroundColor = barBackgroundColor.ToPlatform();
+			}
+
+			if (barBackground is not null)
+			{
+				navigationBarAppearance.BackgroundImage = ((UIView)navBar).GetBackgroundImage(barBackground);
+			}
+
+			navBar.CompactAppearance = navigationBarAppearance;
+			navBar.StandardAppearance = navigationBarAppearance;
+			navBar.ScrollEdgeAppearance = navigationBarAppearance;
+
+			handler.UpdateValue(PlatformConfiguration.iOSSpecific.NavigationPage.HideNavigationBarSeparatorProperty.PropertyName);
+		}
+
+		void OnBarBackgroundBrushInvalidated(object sender, EventArgs e)
+		{
+			if (Handler is NavigationViewHandler handler)
+			{
+				MapBarBackground(handler, this);
+			}
+		}
+
+		static void MapIsNavigationBarTranslucent(NavigationViewHandler handler, NavigationPage navigationPage)
+		{
+			// Translucency affects both bar appearance and content layout;
+			// re-evaluate everything through MapBarBackground.
+			MapBarBackground(handler, navigationPage);
+		}
+
+		static void MapBarTextColor(NavigationViewHandler handler, NavigationPage navigationPage)
+		{
+			var navBar = handler.NavigationController?.NavigationBar;
+
+			if (navBar is null)
+			{
+				return;
+			}
+
+			var barTextColor = navigationPage.BarTextColor;
+
+			var globalTitleTextAttributes = UINavigationBar.Appearance.TitleTextAttributes;
+			var titleTextAttributes = new UIStringAttributes
+			{
+				ForegroundColor = barTextColor is null
+					? globalTitleTextAttributes?.ForegroundColor
+					: barTextColor.ToPlatform(),
+				Font = globalTitleTextAttributes?.Font
+			};
+
+			var largeTitleTextAttributes = titleTextAttributes;
+
+			if (OperatingSystem.IsIOSVersionAtLeast(11))
+			{
+				var globalLargeTitleTextAttributes = UINavigationBar.Appearance.LargeTitleTextAttributes;
+				largeTitleTextAttributes = new UIStringAttributes
+				{
+					ForegroundColor = barTextColor is null
+						? globalLargeTitleTextAttributes?.ForegroundColor
+						: barTextColor.ToPlatform(),
+					Font = globalLargeTitleTextAttributes?.Font
+				};
+			}
+
+			if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
+			{
+				// iOS 26 Liquid Glass: in-place mutation may not trigger updates;
+				// use copy/mutate/reassign pattern.
+				var titleCompact = navBar.CompactAppearance;
+				titleCompact.TitleTextAttributes = titleTextAttributes;
+				titleCompact.LargeTitleTextAttributes = largeTitleTextAttributes;
+				navBar.CompactAppearance = titleCompact;
+
+				var titleStandard = navBar.StandardAppearance;
+				titleStandard.TitleTextAttributes = titleTextAttributes;
+				titleStandard.LargeTitleTextAttributes = largeTitleTextAttributes;
+				navBar.StandardAppearance = titleStandard;
+
+				var titleScrollEdge = navBar.ScrollEdgeAppearance;
+				titleScrollEdge.TitleTextAttributes = titleTextAttributes;
+				titleScrollEdge.LargeTitleTextAttributes = largeTitleTextAttributes;
+				navBar.ScrollEdgeAppearance = titleScrollEdge;
+			}
+			else
+			{
+				navBar.CompactAppearance.TitleTextAttributes = titleTextAttributes;
+				navBar.CompactAppearance.LargeTitleTextAttributes = largeTitleTextAttributes;
+				navBar.StandardAppearance.TitleTextAttributes = titleTextAttributes;
+				navBar.StandardAppearance.LargeTitleTextAttributes = largeTitleTextAttributes;
+				navBar.ScrollEdgeAppearance.TitleTextAttributes = titleTextAttributes;
+				navBar.ScrollEdgeAppearance.LargeTitleTextAttributes = largeTitleTextAttributes;
+			}
+
+			var iconColor = navigationPage.CurrentPage is Page current ? GetIconColor(current) : null;
+			if (iconColor is null)
+			{
+				iconColor = barTextColor;
+			}
+
+			navBar.TintColor = iconColor is null || iOSSpecificNavigationPage.GetStatusBarTextColorMode(navigationPage) == PlatformConfiguration.iOSSpecific.StatusBarTextColorMode.DoNotAdjust
+				? UINavigationBar.Appearance.TintColor
+				: iconColor.ToPlatform();
+
+			// iOS 26+ Liquid Glass ignores TintColor for the back button; apply via appearance instead.
+			if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
+			{
+				var effectiveColor = iconColor ?? barTextColor;
+				var statusBarMode = iOSSpecificNavigationPage.GetStatusBarTextColorMode(navigationPage);
+				var useCustomColor = effectiveColor is not null && statusBarMode != PlatformConfiguration.iOSSpecific.StatusBarTextColorMode.DoNotAdjust;
+
+				if (handler.NavigationController?.VisibleViewController?.NavigationItem?.RightBarButtonItems is UIBarButtonItem[] items)
+				{
+					foreach (var item in items)
+					{
+						item.TintColor = navBar.TintColor;
+					}
+				}
+
+				ApplyBackButtonAppearanceForColor(navBar, effectiveColor, useCustomColor);
+			}
+
+			SetStatusBarStyle(navigationPage);
+		}
+
+		static void MapStatusBarTextColorMode(NavigationViewHandler handler, NavigationPage navigationPage)
+		{
+			SetStatusBarStyle(navigationPage);
+
+			// Matches renderer: StatusBarTextColorMode gates IconColor/TintColor in
+			// MapBarTextColor. Toggling the mode must also refresh bar text appearance,
+			// otherwise the tint stays stale from the previous mode.
+			handler.UpdateValue(nameof(NavigationPage.BarTextColor));
+		}
+
+		static void SetStatusBarStyle(NavigationPage navigationPage)
+		{
+			// Skip if the nav controller's view isn't in the window yet (off-screen tab).
+			if (navigationPage.Handler is NavigationViewHandler nvh
+				&& nvh.NavigationController?.View?.Window is null)
+			{
+				return;
+			}
+
+			var barTextColor = navigationPage.BarTextColor;
+			var statusBarColorMode = iOSSpecificNavigationPage.GetStatusBarTextColorMode(navigationPage);
+
+#pragma warning disable CA1416, CA1422 // 'UIApplication.StatusBarStyle' is unsupported on: 'ios' 9.0 and later
+			if (statusBarColorMode == PlatformConfiguration.iOSSpecific.StatusBarTextColorMode.DoNotAdjust || barTextColor?.GetLuminosity() <= 0.5)
+			{
+				if (OperatingSystem.IsIOSVersionAtLeast(13) || OperatingSystem.IsMacCatalystVersionAtLeast(13))
+				{
+					UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.DarkContent;
+				}
+				else
+				{
+					UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.Default;
+				}
+			}
+			else
+			{
+				UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent;
+			}
+#pragma warning restore CA1416, CA1422
+		}
+
+		static void MapPrefersHomeIndicatorAutoHidden(NavigationViewHandler handler, NavigationPage navigationPage)
+		{
+			if (handler is IPlatformViewHandler pvh && pvh.ViewController is UINavigationController navController)
+			{
+				navController.SetNeedsUpdateOfHomeIndicatorAutoHidden();
+			}
+		}
+
+		static void MapPrefersStatusBarHidden(NavigationViewHandler handler, NavigationPage navigationPage)
+		{
+			if (handler is IPlatformViewHandler pvh && pvh.ViewController is UINavigationController navController)
+			{
+				navController.SetNeedsStatusBarAppearanceUpdate();
+			}
+		}
+
+		static void MapPreferredStatusBarUpdateAnimation(NavigationViewHandler handler, NavigationPage navigationPage)
+		{
+			var animation = PlatformConfiguration.iOSSpecific.Page.PreferredStatusBarUpdateAnimation(
+				navigationPage.OnThisPlatform());
+
+			if (navigationPage.CurrentPage is Page current)
+			{
+				PlatformConfiguration.iOSSpecific.Page.SetPreferredStatusBarUpdateAnimation(
+					current.OnThisPlatform(), animation);
+			}
+
+			if (handler is IPlatformViewHandler pvh && pvh.ViewController is UINavigationController navController)
+			{
+				navController.SetNeedsStatusBarAppearanceUpdate();
+			}
+		}
+
+		static void MapHideNavigationBarSeparator(NavigationViewHandler handler, NavigationPage navigationPage)
+		{
+			var navBar = handler.NavigationController?.NavigationBar;
+
+			if (navBar is null)
+			{
+				return;
+			}
+
+			bool shouldHide = iOSSpecificNavigationPage.GetHideNavigationBarSeparator(navigationPage);
+			var shadowColor = shouldHide ? UIColor.Clear : UIColor.FromRGBA(0, 0, 0, 76);
+
+			// Use copy/mutate/reassign pattern — in-place mutation is not detected
+			// by UIKit on iOS 26 Liquid Glass.
+			var compact = navBar.CompactAppearance;
+			compact.ShadowColor = shadowColor;
+			navBar.CompactAppearance = compact;
+
+			var standard = navBar.StandardAppearance;
+			standard.ShadowColor = shadowColor;
+			navBar.StandardAppearance = standard;
+
+			var scrollEdge = navBar.ScrollEdgeAppearance;
+			scrollEdge.ShadowColor = shadowColor;
+			navBar.ScrollEdgeAppearance = scrollEdge;
+		}
+
+		/// <summary>
+		/// Bridges legacy UINavigationBar API values to the modern UINavigationBarAppearance API.
+		/// Matches renderer's SetupDefaultNavigationBarAppearance() — preserves native background,
+		/// shadow, and back-indicator images set via UINavigationBar.Appearance proxy (pre-iOS 13 pattern).
+		/// Only fills values that the appearance doesn't already have (null checks).
+		/// </summary>
+		static void SetupDefaultNavigationBarAppearance(UINavigationBar navBar, UINavigationBarAppearance appearance)
+		{
+			if (appearance.BackgroundColor is null)
+			{
+				appearance.BackgroundColor = navBar.BarTintColor;
+			}
+
+			if (appearance.BackgroundImage is null)
+			{
+				appearance.BackgroundImage = navBar.GetBackgroundImage(UIBarMetrics.Default);
+			}
+
+			if (appearance.ShadowImage is null)
+			{
+				var shadowImage = navBar.ShadowImage;
+				appearance.ShadowImage = shadowImage;
+
+				if (shadowImage is not null && shadowImage.Size == CoreGraphics.CGSize.Empty)
+				{
+					appearance.ShadowColor = UIColor.Clear;
+				}
+			}
+
+			var backIndicatorImage = navBar.BackIndicatorImage;
+			var backIndicatorMask = navBar.BackIndicatorTransitionMaskImage;
+
+			appearance.SetBackIndicatorImage(backIndicatorImage, backIndicatorMask);
+		}
+
+		/// <summary>
+		/// iOS 26+ Liquid Glass: applies or resets BackButtonAppearance and BackIndicatorImage
+		/// on all nav bar appearance states. Shared by MapBarTextColor and UpdateTintColorForPage.
+		/// </summary>
+		internal static void ApplyBackButtonAppearanceForColor(UINavigationBar navBar, Graphics.Color effectiveColor, bool useCustomColor)
+		{
+			if (useCustomColor)
+			{
+				var backColor = effectiveColor!.ToPlatform();
+				var colorAttributes = Foundation.NSDictionary<Foundation.NSString, Foundation.NSObject>.FromObjectsAndKeys(
+					new Foundation.NSObject[] { backColor }, new Foundation.NSString[] { UIStringAttributeKey.ForegroundColor });
+				var btnAppearance = new UIBarButtonItemAppearance(UIBarButtonItemStyle.Plain);
+				btnAppearance.Normal.TitleTextAttributes = colorAttributes;
+				btnAppearance.Highlighted.TitleTextAttributes = colorAttributes;
+
+				UIImage tintedImage = null;
+				var backImage = UIImage.GetSystemImage("chevron.backward");
+
+				if (backImage is not null)
+				{
+					tintedImage = backImage.ApplyTintColor(backColor).ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
+					navBar.BackIndicatorImage = tintedImage;
+					navBar.BackIndicatorTransitionMaskImage = tintedImage;
+				}
+
+				var compactAppearance = navBar.CompactAppearance;
+				if (compactAppearance is not null)
+				{
+					compactAppearance.BackButtonAppearance = btnAppearance;
+
+					if (tintedImage is not null)
+					{
+						compactAppearance.SetBackIndicatorImage(tintedImage, tintedImage);
+					}
+					navBar.CompactAppearance = compactAppearance;
+				}
+
+				var standardAppearance = navBar.StandardAppearance;
+				if (standardAppearance is not null)
+				{
+					standardAppearance.BackButtonAppearance = btnAppearance;
+
+					if (tintedImage is not null)
+					{
+						standardAppearance.SetBackIndicatorImage(tintedImage, tintedImage);
+					}
+					navBar.StandardAppearance = standardAppearance;
+				}
+
+				var scrollEdgeAppearance = navBar.ScrollEdgeAppearance;
+				if (scrollEdgeAppearance is not null)
+				{
+					scrollEdgeAppearance.BackButtonAppearance = btnAppearance;
+
+					if (tintedImage is not null)
+					{
+						scrollEdgeAppearance.SetBackIndicatorImage(tintedImage, tintedImage);
+					}
+					navBar.ScrollEdgeAppearance = scrollEdgeAppearance;
+				}
+			}
+			else
+			{
+				navBar.BackIndicatorImage = UINavigationBar.Appearance.BackIndicatorImage;
+				navBar.BackIndicatorTransitionMaskImage = UINavigationBar.Appearance.BackIndicatorTransitionMaskImage;
+
+				var globalBackIndicator = navBar.BackIndicatorImage;
+				var globalBackMask = navBar.BackIndicatorTransitionMaskImage;
+
+				var compactAppearance = navBar.CompactAppearance;
+				if (compactAppearance is not null)
+				{
+					compactAppearance.BackButtonAppearance = UINavigationBar.Appearance.CompactAppearance?.BackButtonAppearance
+						?? new UIBarButtonItemAppearance(UIBarButtonItemStyle.Plain);
+					compactAppearance.SetBackIndicatorImage(globalBackIndicator, globalBackMask);
+					navBar.CompactAppearance = compactAppearance;
+				}
+
+				var standardAppearance = navBar.StandardAppearance;
+				if (standardAppearance is not null)
+				{
+					standardAppearance.BackButtonAppearance = UINavigationBar.Appearance.StandardAppearance?.BackButtonAppearance
+						?? new UIBarButtonItemAppearance(UIBarButtonItemStyle.Plain);
+					standardAppearance.SetBackIndicatorImage(globalBackIndicator, globalBackMask);
+					navBar.StandardAppearance = standardAppearance;
+				}
+
+				var scrollEdgeAppearance = navBar.ScrollEdgeAppearance;
+				if (scrollEdgeAppearance is not null)
+				{
+					scrollEdgeAppearance.BackButtonAppearance = UINavigationBar.Appearance.ScrollEdgeAppearance?.BackButtonAppearance
+						?? new UIBarButtonItemAppearance(UIBarButtonItemStyle.Plain);
+					scrollEdgeAppearance.SetBackIndicatorImage(globalBackIndicator, globalBackMask);
+					navBar.ScrollEdgeAppearance = scrollEdgeAppearance;
+				}
+			}
 		}
 	}
 }
\ No newline at end of file
diff --git a/src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cs b/src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cs
new file mode 100644
index 0000000000..b18f829781
--- /dev/null
+++ b/src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cs
@@ -0,0 +1,1179 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using CoreGraphics;
+using Microsoft.Maui.Controls.Compatibility.Platform.iOS;
+using Microsoft.Maui.Controls.Internals;
+using Microsoft.Maui.Graphics;
+using Microsoft.Maui.Graphics.Platform;
+using Microsoft.Maui.Layouts;
+using UIKit;
+using PointF = CoreGraphics.CGPoint;
+using RectangleF = CoreGraphics.CGRect;
+
+namespace Microsoft.Maui.Controls
+{
+    /// <summary>
+    /// Wrapper VC used by NavigationViewHandler (handler architecture).
+    /// Mirrors the renderer's ParentingViewController: manages toolbar items,
+    /// nav bar visibility, back button, title, and per-page property changes.
+    /// </summary>
+    sealed class NavigationHandlerParentingViewController : UIViewController
+    {
+        WeakReference<Page>? _childRef;
+        ToolbarTracker _tracker = new();
+        List<ToolbarItem> _trackedToolbarItems = new();
+        bool _toolbarUpdatePending;
+        bool _disposed;
+
+        static string? _defaultAccessibilityLabel;
+        static string? _defaultAccessibilityHint;
+
+        public NavigationHandlerParentingViewController()
+        {
+        }
+
+        public Page? Child
+        {
+            get => _childRef?.TryGetTarget(out var p) == true ? p : null;
+            set
+            {
+                var old = Child;
+
+                if (old == value)
+                {
+                    return;
+                }
+
+                old?.PropertyChanged -= HandleChildPropertyChanged;
+
+                if (value is not null)
+                {
+                    _childRef = new WeakReference<Page>(value);
+                    value.PropertyChanged += HandleChildPropertyChanged;
+                }
+                else
+                {
+                    _childRef = null;
+                }
+
+                UpdateHasBackButton();
+                UpdateLargeTitles();
+            }
+        }
+
+        public override void ViewDidLoad()
+        {
+            base.ViewDidLoad();
+
+            // Set a system background so this VC isn't transparent when the child
+            // view is hidden (e.g., FlyoutPage.IsVisible = false).
+            View!.BackgroundColor = UIColor.SystemBackground;
+
+            if (Child is Page child)
+            {
+                var parentPages = child.GetParentPages();
+                var flyoutPageWithToolbarItems = FindFlyoutPageWithToolbarItems(parentPages);
+
+                if (flyoutPageWithToolbarItems is not null)
+                {
+                    _tracker.Target = flyoutPageWithToolbarItems.Flyout;
+                    var additionalTargets = new List<Page>(parentPages) { child };
+                    _tracker.AdditionalTargets = additionalTargets;
+                }
+                else
+                {
+                    _tracker.Target = child;
+                    _tracker.AdditionalTargets = parentPages;
+                }
+
+                _tracker.CollectionChanged += TrackerOnCollectionChanged;
+
+                NavigationItem.Title = child.Title ?? GetNavigationPageTitle(child);
+                UpdateBackButtonTitle();
+                UpdateToolbarItems();
+                UpdateLeftBarButtonItem();
+            }
+        }
+
+        /// <summary>
+        /// Called by the handler after a mid-stack insert/remove to re-evaluate
+        /// the left bar button item (flyout icon vs back button).
+        /// </summary>
+        internal void NotifyStackChanged()
+        {
+            UpdateLeftBarButtonItem();
+        }
+
+        public override UIViewController ChildViewControllerForHomeIndicatorAutoHidden =>
+            (Child?.Handler as IPlatformViewHandler)?.ViewController ?? this;
+
+        public override UIViewController ChildViewControllerForStatusBarHidden() =>
+            (Child?.Handler as IPlatformViewHandler)?.ViewController ?? this;
+
+        public override bool PrefersStatusBarHidden()
+        {
+            if ((Child?.Handler as IPlatformViewHandler)?.ViewController is UIViewController childVC)
+            {
+                return childVC.PrefersStatusBarHidden();
+            }
+            return base.PrefersStatusBarHidden();
+        }
+
+        public override bool PrefersHomeIndicatorAutoHidden
+        {
+            get
+            {
+                if ((Child?.Handler as IPlatformViewHandler)?.ViewController is UIViewController childVC)
+                {
+                    return childVC.PrefersHomeIndicatorAutoHidden;
+                }
+                return base.PrefersHomeIndicatorAutoHidden;
+            }
+        }
+
+        public override UIStatusBarAnimation PreferredStatusBarUpdateAnimation =>
+            (Child?.Handler as IPlatformViewHandler)?.ViewController?.PreferredStatusBarUpdateAnimation
+            ?? base.PreferredStatusBarUpdateAnimation;
+
+        public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations()
+        {
+            if (Child?.Handler is IPlatformViewHandler ivh)
+                return ivh.ViewController!.GetSupportedInterfaceOrientations();
+            return base.GetSupportedInterfaceOrientations();
+        }
+
+        public override UIInterfaceOrientation PreferredInterfaceOrientationForPresentation()
+        {
+            if (Child?.Handler is IPlatformViewHandler ivh)
+                return ivh.ViewController!.PreferredInterfaceOrientationForPresentation();
+            return base.PreferredInterfaceOrientationForPresentation();
+        }
+
+#pragma warning disable CA1422 // ShouldAutorotate is deprecated on iOS 16+
+        public override bool ShouldAutorotate()
+        {
+            if (Child?.Handler is IPlatformViewHandler ivh)
+                return ivh.ViewController!.ShouldAutorotate();
+            return base.ShouldAutorotate();
+        }
+#pragma warning restore CA1422
+
+        [System.Runtime.Versioning.UnsupportedOSPlatform("ios6.0")]
+        [System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
+        public override bool ShouldAutorotateToInterfaceOrientation(UIInterfaceOrientation toInterfaceOrientation)
+        {
+            if (Child?.Handler is IPlatformViewHandler ivh)
+                return ivh.ViewController!.ShouldAutorotateToInterfaceOrientation(toInterfaceOrientation);
+            return base.ShouldAutorotateToInterfaceOrientation(toInterfaceOrientation);
+        }
+
+        public override bool ShouldAutomaticallyForwardRotationMethods => true;
+
+        public override void ViewWillAppear(bool animated)
+        {
+            UpdateNavigationBarVisibility(animated);
+
+            // Match renderer behavior: when the nav bar is opaque, prevent content
+            // from extending underneath it. When translucent, allow full extension.
+            var isTranslucent = NavigationController?.NavigationBar.Translucent ?? false;
+            EdgesForExtendedLayout = isTranslucent ? UIRectEdge.All : UIRectEdge.None;
+
+            // Re-evaluate per-page IconColor when this page becomes visible
+            // (push or pop-back). IconColor is already set before the push,
+            // so HandleChildPropertyChanged won't fire — we need this trigger.
+            UpdateIconColor();
+
+            // Override stale TintColor from UpdateIconColor — during native back pops,
+            // CurrentPage hasn't updated yet. Use this VC's Child page directly.
+            UpdateTintColorForPage();
+
+            // Re-evaluate flyout button when this page appears (e.g., Detail is switched
+            // back to an already-loaded NavigationPage in a FlyoutPage).
+            UpdateLeftBarButtonItem();
+
+            base.ViewWillAppear(animated);
+        }
+
+        public override void ViewWillLayoutSubviews()
+        {
+            base.ViewWillLayoutSubviews();
+
+            var childView = (Child?.Handler as IPlatformViewHandler)?.ViewController?.View;
+            childView?.Frame = View!.Bounds;
+        }
+
+        public override void ViewDidDisappear(bool animated)
+        {
+            base.ViewDidDisappear(animated);
+
+            // Force redraw for right toolbar items to prevent them being grayed out
+            // after canceling swipe-to-go-back
+            if (NavigationItem?.RightBarButtonItems is UIBarButtonItem[] items)
+            {
+                foreach (var item in items)
+                {
+                    if (item.Image is not null)
+                    {
+                        continue;
+                    }
+
+                    var tintColor = item.TintColor;
+                    item.TintColor = tintColor is null ? UIColor.Clear : null;
+                    item.TintColor = tintColor;
+                }
+            }
+        }
+
+        public override void ViewWillTransitionToSize(CGSize toSize, IUIViewControllerTransitionCoordinator coordinator)
+        {
+            base.ViewWillTransitionToSize(toSize, coordinator);
+
+            if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad &&
+                (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)))
+            {
+                coordinator.AnimateAlongsideTransition(_ =>
+                {
+                    UpdateTitleViewFrameForOrientation();
+                }, null);
+            }
+        }
+
+#pragma warning disable CA1422 // TraitCollectionDidChange is deprecated on iOS 17+
+        public override void TraitCollectionDidChange(UITraitCollection? previousTraitCollection)
+        {
+            base.TraitCollectionDidChange(previousTraitCollection);
+
+            if ((OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) &&
+                (previousTraitCollection?.VerticalSizeClass != TraitCollection.VerticalSizeClass ||
+                 previousTraitCollection?.HorizontalSizeClass != TraitCollection.HorizontalSizeClass))
+            {
+                UpdateTitleViewFrameForOrientation();
+            }
+        }
+#pragma warning restore CA1422
+
+        void UpdateTitleViewFrameForOrientation()
+        {
+            if (NavigationItem?.TitleView is not UIView titleView)
+            {
+                return;
+            }
+
+            if (NavigationController?.NavigationBar is UINavigationBar navBar)
+            {
+                var frame = navBar.Frame;
+                titleView.Frame = new RectangleF(0, 0, frame.Width, frame.Height);
+                titleView.LayoutIfNeeded();
+            }
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            _disposed = true;
+
+            if (disposing)
+            {
+                ClearTitleViewContainer();
+                CleanToolbarItems();
+
+                // Dispose the final set of native bar button items to prevent
+                // native peer accumulation (they're not disposed by CleanToolbarItems).
+                if (NavigationItem.RightBarButtonItems is UIBarButtonItem[] rightItems)
+                {
+                    NavigationItem.RightBarButtonItems = null;
+                    foreach (var item in rightItems)
+                    {
+                        item.Dispose();
+                    }
+                }
+
+                if (ToolbarItems is UIBarButtonItem[] toolbarItems)
+                {
+                    ToolbarItems = null;
+                    foreach (var item in toolbarItems)
+                    {
+                        item.Dispose();
+                    }
+                }
+
+                // Properly detach child view controllers added via AddChildViewController
+                // in CreateForPage. The renderer's ParentingViewController.Disconnect
+                // explicitly removed each child VC before disposal. 
+                if (ChildViewControllers is UIViewController[] children)
+                {
+                    foreach (var childVC in children)
+                    {
+                        childVC.WillMoveToParentViewController(null);
+                        childVC.View?.RemoveFromSuperview();
+                        childVC.RemoveFromParentViewController();
+                    }
+                }
+
+                if (Child is Page child)
+                {
+                    child.PropertyChanged -= HandleChildPropertyChanged;
+                    _childRef = null;
+                }
+
+                if (_tracker is not null)
+                {
+                    _tracker.Target = null;
+                    _tracker.CollectionChanged -= TrackerOnCollectionChanged;
+                    _tracker = null!;
+                }
+            }
+
+            base.Dispose(disposing);
+        }
+
+        /// <summary>
+        /// Called by the NavigationPage Title mapper when NavigationPage.Title changes.
+        /// Updates the nav bar title if child.Title is null (uses NavigationPage.Title as fallback).
+        /// </summary>
+        internal void RefreshTitleFromNavigationPage()
+        {
+            if (Child is Page child && child.Title is null)
+            {
+                NavigationItem.Title = GetNavigationPageTitle(child);
+            }
+        }
+
+        void HandleChildPropertyChanged(object? sender, PropertyChangedEventArgs e)
+        {
+            if (e.PropertyName == NavigationPage.HasNavigationBarProperty.PropertyName)
+            {
+                UpdateNavigationBarVisibility(true);
+            }
+            else if (e.PropertyName == Page.TitleProperty.PropertyName)
+            {
+                NavigationItem.Title = Child?.Title ?? GetNavigationPageTitle(Child);
+            }
+            else if (e.PropertyName == NavigationPage.HasBackButtonProperty.PropertyName)
+            {
+                UpdateHasBackButton();
+            }
+            else if (e.PropertyName == NavigationPage.BackButtonTitleProperty.PropertyName)
+            {
+                UpdateBackButtonTitle();
+            }
+            else if (e.PropertyName == PlatformConfiguration.iOSSpecific.Page.LargeTitleDisplayProperty.PropertyName)
+            {
+                UpdateLargeTitles();
+            }
+            else if (e.PropertyName == NavigationPage.IconColorProperty.PropertyName)
+            {
+                UpdateIconColor();
+            }
+            else if (e.PropertyName == NavigationPage.BackButtonAccessibilityLabelProperty.PropertyName)
+            {
+                UpdateBackButtonTitle();
+            }
+            else if (e.PropertyName == NavigationPage.TitleViewProperty.PropertyName ||
+                     e.PropertyName == NavigationPage.TitleIconImageSourceProperty.PropertyName)
+            {
+                UpdateTitleArea();
+            }
+        }
+
+        void UpdateNavigatio
... [truncated]

The diff was truncated to fit GitHub's review body limit.

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 28 out of 28 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

src/Core/src/Platform/iOS/NavigationControllerManager/NavigationControllerManager.cs:477

  • INavigationManagerDelegate.ShouldPop() is documented as being called for both back button and interactive-pop. However, the interactive-pop gesture delegate always returns true and never consults ShouldPop(), so swipe-back cannot be blocked.
            public override bool ShouldBegin(UIGestureRecognizer recognizer)
            {
                if (!_navigationControllerRef.TryGetTarget(out var navController))
                {
                    return false;
                }

                // Only allow interactive pop if there's more than the root VC
                if ((navController.ViewControllers?.Length ?? 0) <= 1)
                {
                    return false;
                }

                // Always allow the gesture — matches the renderer, which returns true
                // and lets UIKit drive the interactive transition. After the gesture
                // completes, OnInteractivePopCompleted syncs the MAUI stack.
                return true;
            }

src/Controls/src/Core/NavigationPage/NavigationPage.iOS.cs:204

  • This calls MapBarBackground directly in response to brush invalidation. This bypasses handler mapper extensibility (AppendToMapping/PrependToMapping). Property updates should go through handler.UpdateValue(...) instead.
		void OnBarBackgroundBrushInvalidated(object sender, EventArgs e)
		{
			if (Handler is NavigationViewHandler handler)
			{
				MapBarBackground(handler, this);
			}
		}

Comment thread src/Core/src/Handlers/NavigationPage/NavigationViewHandler.iOS.cs

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 28 out of 28 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cs:399

  • UpdateNavigationBarVisibility only updates current.IgnoresContainerArea when it also toggles NavigationBarHidden. If the nav bar is already in the desired visibility state (e.g., a page starts with HasNavigationBar=false and the controller is already hidden), IgnoresContainerArea won’t be refreshed and can remain incorrect for layout/Insets calculations. This logic is also hard to read because it compares NavigationBarHidden to hasNavBar (inverted).
            if (NavigationController.NavigationBarHidden == hasNavBar)
            {
                current.IgnoresContainerArea = !hasNavBar;
                NavigationController.SetNavigationBarHidden(!hasNavBar, animated);
            }

src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cs:820

  • UpdateToolbarItems disposes the existing UIViewController.ToolbarItems array but leaves the ToolbarItems property still pointing at disposed UIBarButtonItems. That can lead to double-dispose later (Dispose() also disposes ToolbarItems) and risks UIKit referencing disposed items if the toolbar is ever shown.
            if (ToolbarItems is UIBarButtonItem[] oldToolbar)

@vishnumenon2684

Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines

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

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

Could you please check the ai's suggestions and see if test failures are related?

@Tamilarasan-Paranthaman

Copy link
Copy Markdown
Member Author

Could you please check the ai's suggestions and see if test failures are related?

@kubaflo, There are no test failures in iOS and Mac Catalyst. Other failures are not related

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

Labels

area-controls-navigationpage community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/ios platform/macos macOS / Mac Catalyst s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) t/enhancement ☀️ New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants