Last Tuesday I pushed what should have been a routine TestFlight build for a customer's logistics app — a .NET MAUI 9 codebase I had migrated to .NET 10 the week before. Twenty minutes later their lead driver sent me a screenshot from an iPhone 15 Pro running iOS 18.2: the entire app was floating in a letterbox, with a fat black bar across the top and another across the bottom. The splash screen filled the display fine. The login page did not. Nothing about my XAML had changed.
If you are reading this because your .NET MAUI iOS 18 build is not filling the screen on edge-to-edge iPhones, you are almost certainly looking at the same regression I spent four hours debugging that night. The short version is that Apple quietly changed how the safe-area is reported to UIKit hosts in iOS 18, and MAUI's default Page wrapper now honours those insets in a way it did not in iOS 17. The fix is two lines of code. Getting to those two lines, however, took me through three GitHub issues, a chunk of UIKit source via Hopper, and one very confused Slack thread with another MAUI engineer who thought I was hallucinating the bug.
The Symptom: Black Bars on Every Page After iOS 18
The reproduction is dead simple. Take any MAUI ContentPage that previously filled the screen on iOS 17, deploy it to an iPhone with a Dynamic Island or notch running iOS 18.0 or later — iPhone 14 Pro and newer, or any iPhone running iOS 26 in 2026 — and you will see one of two things:
- Pages with a NavigationPage parent render correctly under the nav bar but show a black strip at the bottom above the home indicator.
- Pages presented modally, or pages without a NavigationPage, get black bars on both top and bottom, and the page content is vertically centred in the remaining window.
The black bars are not your BackgroundColor bleeding through — they are literally the UIWindow background showing because the UIViewController's view does not extend into the safe-area regions. If you set Application.Current.UserAppTheme = AppTheme.Dark versus Light, the bars stay black regardless. That was my first clue this was a UIKit-level positioning issue and not a colour or theming bug.
On Android the same XAML rendered edge-to-edge. On iOS 17 simulators the same binary rendered edge-to-edge. Only iOS 18+ devices and simulators reproduced it. I checked my Info.plist, my LaunchScreen.storyboard, my UIRequiresFullScreen key — all unchanged from the version that shipped fine in October.
What Apple Actually Changed in iOS 18
The relevant change is buried in Apple's UIViewController additionalSafeAreaInsets documentation and discussed obliquely in the WWDC24 session "What's new in UIKit." Prior to iOS 18, a UIViewController whose root view was a UIScrollView or a layout container would frequently have its safe-area insets collapsed to zero when it determined the content was responsible for its own inset handling. The behaviour was inconsistent and Apple flagged it as a bug for years.
In iOS 18 they fixed it. Safe-area insets are now reported consistently and the system expects you, the host, to opt into ignoring them if you want edge-to-edge rendering. The problem for us is that .NET MAUI's iOS handler for ContentPage wraps your content in a UIViewController that respects safe-area by default, and the layout engine treats the safe area as a hard padding rather than as an overlay region. Pre-iOS 18 the insets were often zero so nothing happened. Post-iOS 18 the insets are accurate, so MAUI dutifully pads your page away from the edges and the UIWindow background — black — shows through.
I confirmed this by adding a one-line diagnostic in a custom handler:
#if IOS
using UIKit;
public partial class DiagnosticPageHandler : Microsoft.Maui.Handlers.PageHandler
{
protected override void ConnectHandler(Microsoft.Maui.Platform.ContentView nativeView)
{
base.ConnectHandler(nativeView);
var insets = nativeView.SafeAreaInsets;
System.Diagnostics.Debug.WriteLine(
$"[SafeArea] top={insets.Top} bottom={insets.Bottom} " +
$"left={insets.Left} right={insets.Right}");
}
}
#endif
On iOS 17.5 simulator: top=0 bottom=0. On iOS 18.2 device: top=59 bottom=34. There was the regression in one line of output.
The Fix: SafeAreaEdges and Ignoring Safe Area at the Page Level
MAUI exposes the iOS-specific safe-area behaviour via the Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.Page attached property documented on Microsoft Learn's iOS Safe Area Layout Guide page. You call SetUseSafeArea(false) on the page, and MAUI will tell the underlying UIViewController to extend its layout into the safe area regions. Your content then needs to manage its own padding so that buttons do not end up under the Dynamic Island.
For a single page, do this in code-behind:
using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
public partial class LoginPage : ContentPage
{
public LoginPage()
{
InitializeComponent();
On<iOS>().SetUseSafeArea(false);
}
}
Or, the way I prefer for a project-wide fix, do it in XAML on a base page so every screen inherits the behaviour:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
x:Class="LogisticsApp.Pages.BasePage"
ios:Page.UseSafeArea="False">
<!-- child content goes here -->
</ContentPage>
Now your page fills the display. But you have a second problem: anything at the very top of your visual tree is now under the status bar and Dynamic Island, and anything at the bottom is under the home indicator. You need to add that padding back manually, but only on iOS and only by the actual reported insets — hardcoding 44 or 47 pixels will look wrong on the next generation of devices.
The clean way is to read the insets at runtime and bind them to a Thickness on your root layout. I ship this as a small markup extension:
public static class SafeAreaHelper
{
public static Thickness Current
{
get
{
#if IOS
var window = UIKit.UIApplication.SharedApplication
.ConnectedScenes
.OfType<UIKit.UIWindowScene>()
.SelectMany(s => s.Windows)
.FirstOrDefault(w => w.IsKeyWindow);
if (window?.SafeAreaInsets is { } i)
return new Thickness(i.Left, i.Top, i.Right, i.Bottom);
#endif
return new Thickness(0);
}
}
}
Then wrap your root layout in a Grid with that padding applied. I tend to leave the top edge unpadded for hero images and only pad the bottom, but that is design-dependent.
Caveats I Hit in Production
Three things bit me after the initial fix landed. The first was orientation changes. SafeAreaInsets are reported relative to the current interface orientation, so a value you cached at app launch is wrong the moment the user rotates. If you support landscape, hook UIView.SafeAreaInsetsDidChange on your root native view or subscribe to Microsoft.Maui.Devices.DeviceDisplay.MainDisplayInfoChanged and recompute.
The second was keyboard avoidance. Once you turn off MAUI's safe-area handling, the platform-default keyboard adjustment still works but the calculations change because your page now extends to the bottom of the screen rather than to the top of the home indicator. Entry fields near the bottom can end up sitting flush against the keyboard. I fixed this by adding an explicit 16-point bottom padding to my form layouts and using a small Entry handler customisation to nudge the focused field into view.
The third caveat was Shell apps. If you use Shell as your navigation root, the Shell renderer handles safe-area at the flyout and tab-bar level itself. Setting UseSafeArea="False" on individual ShellContent pages works, but if you set it on the Shell itself you will get the tab bar floating over content. Leave the Shell alone and only apply the attribute to the child ContentPages.
One more nuance worth mentioning: the related SafeAreaEdges property that appears in some older Xamarin.Forms guides does not exist on MAUI's iOSSpecific Page class. If you are searching Stack Overflow you will find advice telling you to set Page.SafeAreaEdges="None" in XAML — that is the Xamarin.Forms API and it was not ported. Use UseSafeArea as shown above. There is an open tracking issue on dotnet/maui requesting per-edge control, but as of .NET 10 GA the only switch is on-or-off for the whole page.
Verifying the Fix Across Devices
Before I shipped to TestFlight again I ran the fixed build through a matrix of simulators and one real device per generation. On the iOS 18.2 simulator for iPhone 16 Pro the login page now extends to all four edges. On the iPhone SE 3rd-gen simulator running iOS 18.2 the page also fills correctly because there is no safe-area to ignore — the insets are zero on Touch ID devices and the override is a no-op. On an iPad Air running iPadOS 18 in split-view the page extends to the window edges as expected.
I also pulled up the app in the Xcode View Hierarchy Debugger to confirm the root UIViewController's view bounds matched the UIWindow bounds. They did. The black bars were gone. Total time from "you broke production" to "fixed build in their hands": about five hours, four of which were spent not knowing about SetUseSafeArea. If you have a MAUI project that has not been touched since before iOS 18 dropped, this is going to bite you the moment you rebuild it against the current Xcode SDK.
If you maintain multiple MAUI apps, I would also recommend reading my notes on .NET MAUI iOS build broken after Xcode 16 upgrade and the related piece on MAUI Shell flyout rendering under the iOS status bar — both are downstream symptoms of the same iOS 18 safe-area overhaul and the fixes share DNA with what is described above.
FAQ
Does this affect Android too?
No. The change is entirely in UIKit's safe-area reporting. Android uses WindowInsets which MAUI handles through a different code path. I have not seen any Android-side regressions from the same MAUI version that exhibits the iOS bug.
Will setting UseSafeArea to false break older iOS versions?
It will not. On iOS 17 and earlier the call is still respected — your page extends edge-to-edge there too, which is what most designs want. The insets are reported as zero on pre-notch hardware, so devices like the iPhone SE behave identically with or without the flag.
Why did Microsoft not fix this in MAUI directly?
It is debated on the issue tracker. The argument against changing the default is that thousands of shipping apps rely on the current safe-area padding for their headers and toolbars. Flipping the default would silently break those layouts. Microsoft's position, reasonable in my view, is that opt-in via UseSafeArea="False" is the right migration path for apps that want true edge-to-edge.
Is there a global setting instead of per-page?
Not directly. The cleanest project-wide approach is to define a base ContentPage class — either in XAML as shown above or in C# — that sets UseSafeArea to false in its constructor, and have every page in your app inherit from it. That keeps the configuration in one place and lets you opt individual pages back in if needed.