A Microsoft open-source framework for building native device applications spanning mobile, tablet, and desktop.
Hi @Kurt K ,
Thanks for putting the repo together and testing on a real device.
First, a quick correction on my earlier wording. By "light purple" I meant the color you see around the hamburger icon, not the Shell.BackgroundColor value. #521B3B was the right value, so your test was set up correctly. On iOS 26 the bar button sits on a Liquid Glass background, which is likely why the area around the icon looks lighter than #521B3B itself.
The FormSheet vs FullScreen comparison is the key finding. When a sheet is presented, UIKit dims the views underneath it and is expected to restore them once the sheet is gone. After a FormSheet is dismissed, the hamburger button in the Shell navigation bar appears to stay in that dimmed state. Going to the Sub Page and back fixes it because the navigation bar button gets recreated. FullScreen is handled differently by UIKit, which would explain why it doesn't show the issue.
I haven't confirmed this in the MAUI source, but it matches everything you've observed. It also means the issue sits in the interaction between UIKit's sheet presentation and how Shell manages its navigation bar, so there's limited room to fix it cleanly from app code.
Using your sample, I built an iOS-only workaround that keeps FormSheet and swipe-to-dismiss. The idea is to reproduce what the Sub Page round trip does, but automatically when the modal closes:
Hook into the native presentation controller so the swipe-to-dismiss gesture is also detected, not just button dismissal
When the modal starts dismissing, force the tint mode back to Normal
Once it's dismissed, push and immediately pop an empty view controller on the root navigation controller so the hamburger button is rebuilt
Platforms/iOS/NavBarFix.cs
#if IOS
using UIKit;
namespace MauiShellTitleView2;
/// <summary>
/// Mimics what happens when navigating to SubPage and back:
/// push an empty VC -> the hamburger button is removed -> pop immediately ->
/// MAUI recreates the hamburger button -> icon returns to its normal state.
/// </summary>
public static class NavBarFix
{
private static bool _isRefreshing;
public static void ForceNavigationRefresh(bool force = false)
{
if (_isRefreshing) return;
_isRefreshing = true;
try
{
foreach (var scene in UIApplication.SharedApplication.ConnectedScenes.OfType<UIWindowScene>())
{
foreach (var window in scene.Windows)
{
ForceNormalTintMode(window);
if (window.RootViewController != null)
TryRefreshOnController(window.RootViewController, force);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[NavBarFix] Error: {ex.Message}");
}
finally
{
_isRefreshing = false;
}
}
public static void ForceNormalOnAllViews()
{
try
{
foreach (var scene in UIApplication.SharedApplication.ConnectedScenes.OfType<UIWindowScene>())
foreach (var window in scene.Windows)
ForceNormalTintMode(window);
}
catch { }
}
private static void TryRefreshOnController(UIViewController vc, bool force)
{
// Only run on the root page (where the hamburger button lives).
// Never run when the stack has more than one controller,
// otherwise the Back button on pages like SubPage would be destroyed.
if (vc is UINavigationController nav && nav.ViewControllers?.Length == 1)
{
if (!force && nav.PresentedViewController != null)
return;
if (nav.View != null)
ForceNormalTintMode(nav.View);
// 1. Reassign the left bar button item
var item = nav.TopViewController?.NavigationItem;
if (item?.LeftBarButtonItem != null)
{
var btn = item.LeftBarButtonItem;
item.LeftBarButtonItem = null;
item.LeftBarButtonItem = btn;
}
// 2. Push an empty VC without animation, then pop it immediately
nav.PushViewController(new UIViewController(), false);
nav.PopViewController(false);
// 3. Reapply Normal tint mode on the rebuilt navigation bar
ForceNormalTintMode(nav.NavigationBar);
nav.NavigationBar.SetNeedsLayout();
nav.NavigationBar.LayoutIfNeeded();
return;
}
foreach (var child in vc.ChildViewControllers)
TryRefreshOnController(child, force);
}
public static void ForceNormalTintMode(UIView view)
{
try
{
view.TintAdjustmentMode = UIViewTintAdjustmentMode.Normal;
foreach (var sub in view.Subviews)
ForceNormalTintMode(sub);
}
catch { }
}
}
#endif
Platforms/iOS/GlobalModalWatcher.cs
#if IOS
using UIKit;
using Microsoft.Maui.Controls;
namespace MauiShellTitleView2;
public static class GlobalModalWatcher
{
private static GlobalAdaptiveDelegate? _strongDelegate;
private static bool _isInitialized;
public static void Initialize()
{
if (_isInitialized) return;
_isInitialized = true;
if (Application.Current != null)
{
Application.Current.PageAppearing += OnPageAppearing;
Application.Current.PageDisappearing += OnPageDisappearing;
Application.Current.ModalPopped += OnModalPopped;
}
// Covers modals closed with a button or GoToAsync
if (Shell.Current != null)
Shell.Current.Navigated += OnShellNavigated;
}
private static bool IsModal(Page page) =>
page is ModalPage ||
page is ModalPage_Full ||
Shell.GetPresentationMode(page).HasFlag(PresentationMode.Modal);
private static void OnPageAppearing(object? sender, Page page)
{
if (IsModal(page) || (page.Navigation?.ModalStack.Contains(page) ?? false))
TryAttachDismissDelegate();
}
private static void OnPageDisappearing(object? sender, Page page)
{
if (IsModal(page))
MainThread.BeginInvokeOnMainThread(NavBarFix.ForceNormalOnAllViews);
}
private static void OnModalPopped(object? sender, ModalPoppedEventArgs e) =>
MainThread.BeginInvokeOnMainThread(() => NavBarFix.ForceNavigationRefresh(force: true));
private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
{
bool wasModal = e.Previous?.Location.OriginalString
.Contains("modal", StringComparison.OrdinalIgnoreCase) ?? false;
bool isMainPage = e.Current?.Location.OriginalString.Contains("MainPage") ?? false;
if (wasModal || isMainPage)
MainThread.BeginInvokeOnMainThread(() => NavBarFix.ForceNavigationRefresh(force: true));
}
// Attach a delegate to the presented controller so swipe-to-dismiss is detected
public static void TryAttachDismissDelegate()
{
MainThread.BeginInvokeOnMainThread(async () =>
{
for (int i = 0; i < 15; i++)
{
var pc = GetTopPresentedViewController()?.PresentationController;
if (pc != null)
{
if (pc.Delegate is not GlobalAdaptiveDelegate)
{
_strongDelegate = new GlobalAdaptiveDelegate(pc.Delegate);
pc.Delegate = _strongDelegate;
}
return;
}
await Task.Delay(50);
}
});
}
private static UIViewController? GetTopPresentedViewController()
{
foreach (var scene in UIApplication.SharedApplication.ConnectedScenes.OfType<UIWindowScene>())
{
foreach (var window in scene.Windows)
{
var root = window.RootViewController;
if (root == null) continue;
var top = root;
while (top.PresentedViewController != null)
top = top.PresentedViewController;
if (top != root) return top;
}
}
return null;
}
// Wraps the original delegate and forwards calls to it,
// so MAUI's own dismissal handling still runs
private class GlobalAdaptiveDelegate : UIAdaptivePresentationControllerDelegate
{
private readonly IUIAdaptivePresentationControllerDelegate? _original;
public GlobalAdaptiveDelegate(IUIAdaptivePresentationControllerDelegate? original) =>
_original = original;
public override void WillDismiss(UIPresentationController presentationController)
{
try { _original?.WillDismiss(presentationController); } catch { }
NavBarFix.ForceNormalOnAllViews();
}
public override void DidDismiss(UIPresentationController presentationController)
{
try { _original?.DidDismiss(presentationController); } catch { }
NavBarFix.ForceNavigationRefresh(force: true);
}
}
}
#endif
Platforms/iOS/AppDelegate.cs
using Foundation;
namespace MauiShellTitleView2
{
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override bool FinishedLaunching(UIKit.UIApplication application, NSDictionary launchOptions)
{
var result = base.FinishedLaunching(application, launchOptions);
GlobalModalWatcher.Initialize();
return result;
}
}
}
No changes are needed in MainPage, ModalPage or SubPage. The modal stays on FormSheet, so swipe-to-dismiss is kept.
Please note that this doesn't resolve the behavior consistently in every case yet. It also relies on replacing the presentation controller's delegate and briefly modifying the UIKit navigation stack underneath Shell. Both can interact with how MAUI tracks modal and navigation state, so I'd treat it as something to evaluate rather than a production-ready fix. If you try it, please also check that the Sub Page's Back button still works as expected.
If you found the analysis and workaround helpful, I would greatly appreciate it if you could follow this guide for your confirmation.