[NET11][iOS/MacCatalyst] Replace NavigationRenderer with NavigationViewHandler - #36109
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36109Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36109" |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 3 findings
See inline comments for details.
dd93616 to
6e68418
Compare
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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.ShouldBeginalways returns true when the stack depth > 1, so consumers cannot block the swipe-back gesture viaINavigationManagerDelegate.ShouldPop()(e.g., Shell usesShouldPop()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;
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
PureWeen
left a comment
There was a problem hiding this comment.
Rewritten-head adversarial review — see the new inline finding.
PureWeen
left a comment
There was a problem hiding this comment.
Rewritten-head adversarial review — one new issue
- ❌ The cached top-controller status-bar delegation is never initialized by the root
SetViewControllerspath, so the initial page'sPrefersStatusBarHiddenpreference 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.
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@Tamilarasan-Paranthaman — new AI review results are available based on this last commit:
099b747.
🗂️ 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.cssrc/Controls/src/Core/Compatibility/Handlers/iOS/DisposeHelpers.cssrc/Controls/src/Core/Hosting/AppHostBuilderExtensions.cssrc/Controls/src/Core/NavigationPage/NavigationPage.Legacy.cssrc/Controls/src/Core/NavigationPage/NavigationPage.Mapper.cssrc/Controls/src/Core/NavigationPage/NavigationPage.cssrc/Controls/src/Core/NavigationPage/NavigationPage.iOS.cssrc/Controls/src/Core/VisualElement/VisualElement.cssrc/Core/src/Handlers/NavigationPage/NavigationViewHandler.iOS.cssrc/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txtsrc/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
New files (not reverted):
src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cssrc/Core/src/Platform/iOS/NavigationControllerManager/INavigationManagerDelegate.cssrc/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
NavigationPagerenderer withNavigationViewHandlerand rewires navigation lifecycle callbacks, matching failures around push/pop ordering, back-button callbacks, swipe-back crash, andOnNavigatedTobehavior. - ✗ PR-related — iOS navigation bar/background/translucency coverage (~15 tests): the repeated
Issue17022failures 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
NavigationPageimplementation with an unconditionalNavigationViewHandler, adding sharedNavigationControllerManagerinfrastructure and Controls-layer toolbar/navigation callbacks. - Gate verification already failed on iOS device tests; do not rerun the gate phase. The failed selectors were
CollectionViewTestsandFlyoutPageTests, 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 becauseShouldPop()returnstrueunconditionally instead of honoringSendBackButtonPressed(). - ✗
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_currentTopVCis not initialized for theSetViewControllersroot 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— MacCatalystMauiNavigationBar.RefreshIfNeeded()equivalent is missing after navigation. - ✗
src/Controls/src/Core/Platform/iOS/NavigationViewHandlerToolbarHelper.cs:617—BackButtonAccessibilityLabel-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
NavigationPageis 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.ControlsConfigurationis static and controls all handler instances.
CI Status
- Required-check result:
gh pr checks --requiredunavailable becauseghis unauthenticated. - Public check-runs fallback: red —
Build Analysis,maui-pr, and multiplemaui-pr-uitestslegs are failing/cancelled on head099b747. - 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
ShouldPopItemdelegates toShouldPop(), 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
BackBarButtonItemtitle becomes null, blanking visible text. - MacCatalyst custom titlebar after push/pop:
TitleBarNeedsRefreshhas noDidShowrefresh 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 #2 — PhoneFlyoutPageRenderer 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.
There was a problem hiding this comment.
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 returnstrueand never consultsShouldPop(), 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
MapBarBackgrounddirectly in response to brush invalidation. This bypasses handler mapper extensibility (AppendToMapping/PrependToMapping). Property updates should go throughhandler.UpdateValue(...)instead.
void OnBarBackgroundBrushInvalidated(object sender, EventArgs e)
{
if (Handler is NavigationViewHandler handler)
{
MapBarBackground(handler, this);
}
}
There was a problem hiding this comment.
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)
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
kubaflo
left a comment
There was a problem hiding this comment.
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 |
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 layeredNavigationViewHandlerarchitecture 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
NavigationRendereris a single class that IS aUINavigationController. It owns all navigation logic, bar appearance, toolbar management, FlyoutPage integration, and page lifecycle in one file. This makes it:What Changed
New Files (Core layer —
src/Core/)Handlers/NavigationPage/NavigationViewHandler.iOS.csNavigationViewDelegate,ContainerViewController,NavigationViewHandlerControlsConfigurationPlatform/iOS/NavigationControllerManager/NavigationControllerManager.csMauiNavigationControllersubclass,NavDelegate,GestureDelegate, push/pop TCS trackingPlatform/iOS/NavigationControllerManager/INavigationManagerDelegate.csNew Files (Controls layer —
src/Controls/)Platform/iOS/NavigationViewHandlerToolbarHelper.csNavigationHandlerParentingViewController(per-page wrapper),TitleViewContainer, factoryNavigationPage/NavigationPage.Mapper.csRemapForControls()— registers mappers + setsControlsConfigurationNavigationPage/NavigationPage.iOS.csMapBarBackground,MapBarTextColor,MapHideNavigationBarSeparator, etc.Modified Files
AppHostBuilderExtensions.csAddHandler<NavigationPage, NavigationViewHandler>()for iOS/MacCatalystNavigationPage.csSendHandlerUpdateAsync()+MauiNavigationImpl+ partial method declarations for deferredNavigatedTo(ShouldDeferNavigatedTo,FireDeferredNavigatedTo)NavigationPage.iOS.csNavigatedToimplementation + mapper implementations (MapBarBackground,MapBarTextColor, etc.)NavigationRenderer.csINavigationViewHandler— available for manual fallbackDisposeHelpers.csDisconnectHandler()for root modal page cleanup (non-IDisposable handlers likeNavigationViewHandler)NavigationPageTests.csSetupBuilder(bool includeNavigationViewHandler)toggle for handler vs renderer testsNavigationPageTests.iOS.csArchitecture Overview
Handler Hierarchy
Key Design Decisions
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.NavigationViewHandlerControlsConfigurationrecordNavigationBarType,CreateViewControllerForPage,OnNativePopCompleted,OnControllerAppeared,OnControllerDisappeared+ optionalOnMidStackChanged). Dependency inversion without circular assembly references.NavigationHandlerParentingViewController)UIViewControllerthat manages toolbar items, nav bar visibility, title view, back button, and large titles. Isolates per-page concerns from the navigation stack.NavigationControllerManageras shared classShouldPopIteminterception) into a reusable manager. Currently used byNavigationViewHandler; designed so a future iOS Shell handler could also consume it viaINavigationManagerDelegate.MauiNavigationControllersubclassnavigationBar:shouldPopItem:to support pop-blocking. OverridesViewDidAppear/ViewDidDisappear/ViewDidLayoutSubviewsto fire delegate callbacks for lifecycle.AppHostBuilderExtensions.cs.NavigationRendererstill exists for manual opt-out if needed.Configuration Bridge
Core defines
NavigationViewHandlerControlsConfiguration— a sealed record with callback slots:NavigationBarTyperequired TypeMauiNavigationBarfor MacCatalyst)CreateViewControllerForPagerequired Func<IView, IMauiContext, UIViewController>OnNativePopCompletedrequired Action<IStackNavigationView, IView>OnControllerAppearedrequired Action<IStackNavigationView>ViewDidAppear(loaded + Appearing)OnControllerDisappearedrequired Action<IStackNavigationView>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).INavigationManagerDelegateInterface (8 methods)GetNavigationBarVisibility(UIViewController)(isHidden, animate)for nav bar per VCShouldPop()OnNavigationComplete(UINavigationController, UIViewController)OnWillShowViewController(...)OnInteractivePopCompleted()OnNavigationControllerDidAppear()MauiNavigationController.ViewDidAppearfiredOnNavigationControllerDidDisappear()MauiNavigationController.ViewDidDisappearfiredOnViewDidLayoutSubviews(CGRect)Frameto MAUIKey Behaviors
UIMenuoverflow button instead of bottomUIToolbarUINavigationBar(no custom subclass needed)Appearing/Disappearingfire viaMauiNavigationController.ViewDidAppear/ViewDidDisappearNavigationPage.Framepropagated viaViewDidLayoutSubviews → Arrange()ViewDidAppearfor TabbedPage scenariosNavigationControllerManagerdesigned to be reusable by a future Shell handlerFeature Parity
Full parity with
NavigationRendererincluding:SetFlyoutLeftBarButton)Shared Infrastructure Summary
NavigationControllerManagerNavigationViewHandler(currently)MauiNavigationControllerShouldPopItem),ViewDidAppear/ViewDidDisappearcallbacks,ViewDidLayoutSubviews→ ArrangeINavigationManagerDelegateNavDelegateUINavigationControllerDelegate— resolves push/pop TCS, manages nav bar visibilityGestureDelegateHandler as Default
The handler is registered as the unconditional default in
AppHostBuilderExtensions.cs— no feature flag or opt-in required.What Changes By Default
NavigationPageusesNavigationViewHandlerinstead ofNavigationRendereron iOS/MacCatalystUIMenuoverflow button (instead of a bottomUIToolbar)Appearingfires beforePushViewController(not afterViewDidAppear)NavigatedTois deferred toViewDidAppearviaFireDeferredNavigatedTo(ensuresNavigationProxy.Inneris wired)How to Opt Out
If an app needs to fall back to the renderer, register it manually in
MauiProgram.cs:NavigationRendererremains in the Compatibility layer and continues to implementINavigationViewHandler.Known Behavioral Differences
AppearingtimingViewDidAppear(page visible)PushViewController(page not yet visible)NavigatedTotimingViewDidAppear(renderer lifecycle)ViewDidAppearviaFireDeferredNavigatedToLoadedstate)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
NavigatedToinstead ofAppearingfor post-navigation work that assumes the page is on-screen.Testing
new NavigationPage(false, rootPage)constructorMigration Guidance for Custom Renderer Users
If you subclassed
NavigationRendereror accessed its internals, this section maps the old patterns to the new handler architecture.Old → New Mapping
NavigationRenderer(IS aUINavigationController)NavigationViewHandler(HAS aUINavigationControllerviaNavigationControllerManager)ViewHandler<IStackNavigationView, UIView>, not a VCViewDidAppearINavigationManagerDelegate.OnNavigationControllerDidAppear()MauiNavigationController.ViewDidAppearViewDidDisappearINavigationManagerDelegate.OnNavigationControllerDidDisappear()ViewDidLayoutSubviewsINavigationManagerDelegate.OnViewDidLayoutSubviews(CGRect)NavigationBarhandler.NavigationController?.NavigationBarNavigationControllerpropertyPopViewAsyncoverrideOnNativePopCompletedcallback inControlsConfigurationCreateViewControllerForPageinControlsConfigurationAdapter Pattern for Custom Renderers
If you must preserve custom renderer logic, wrap it using the handler registration:
Toolbar Item Changes
UIBarButtonItemin nav barUIToolbarUIMenuoverflow button in nav barApps relying on the bottom toolbar for secondary items will see them move to a
···overflow menu button in the navigation bar.