Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,24 @@ void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
void LayoutChildren(bool animated)
{
var frame = Element.Bounds.ToCGRect();

// Apply safe area insets to the FlyoutPage container if UseSafeArea is enabled.
// This ensures the Flyout content (e.g., CollectionView) renders below the status bar/notch
// on iOS devices with notches (iPhone X and newer). Without this adjustment, the container
// would start at Y=0, causing content to overlap with the status bar.
// https://github.com/dotnet/maui/issues/29170
if (Element is FlyoutPage flyoutPage && flyoutPage is ISafeAreaView sav &&
!sav.IgnoreSafeArea && OperatingSystem.IsIOSVersionAtLeast(11))
Comment on lines +374 to +375
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The condition Element is FlyoutPage flyoutPage is redundant. Since FlyoutPage is a property that already casts Element to FlyoutPage, and FlyoutPage inherits from Page which implements ISafeAreaView, you can simplify this to:

if (FlyoutPage is ISafeAreaView sav && !sav.IgnoreSafeArea && OperatingSystem.IsIOSVersionAtLeast(11))

This eliminates the unnecessary pattern matching and uses the existing FlyoutPage property.

Suggested change
if (Element is FlyoutPage flyoutPage && flyoutPage is ISafeAreaView sav &&
!sav.IgnoreSafeArea && OperatingSystem.IsIOSVersionAtLeast(11))
if (FlyoutPage is ISafeAreaView sav && !sav.IgnoreSafeArea && OperatingSystem.IsIOSVersionAtLeast(11))

Copilot uses AI. Check for mistakes.
{
var safeAreaTopInset = View.SafeAreaInsets.Top;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

var safeAreaTop = safeAreaInsets.Top;

safeAreaInsets.Top is checked twice, can extract it into a local variable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the PR with the recommended code optimization by extracting safeAreaInsets.Top into a local variable to reduce property access and improve readability.

if (safeAreaTopInset > 0)
{
frame.Y = safeAreaTopInset;
frame.Height -= safeAreaTopInset;
}
}
Comment on lines +374 to +384
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The safe area adjustment in LayoutChildren is not reactive to changes in the UseSafeArea property. If a developer dynamically changes ios:Page.UseSafeArea at runtime, the layout won't update until the next time LayoutChildren is called for other reasons.

Consider adding a property changed handler in HandlePropertyChanged method:

else if (e.PropertyName == PlatformConfiguration.iOSSpecific.Page.UseSafeAreaProperty.PropertyName)
    LayoutChildren(false);

This ensures that toggling the safe area setting at runtime immediately updates the layout.

Copilot uses AI. Check for mistakes.

var flyoutFrame = frame;
nfloat opacity = 1;

Expand Down
81 changes: 81 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue29170.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace Maui.Controls.Sample.Issues;

using System;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;

[XamlCompilation(XamlCompilationOptions.Compile)]
[Issue(IssueTracker.Github, 29170, "First Item in CollectionView Overlaps in FlyoutPage.Flyout on iOS", PlatformAffected.iOS)]
public partial class Issue29170 : Microsoft.Maui.Controls.FlyoutPage
{
public Issue29170()
{
Flyout = new ContentPage
{
Title = "Menu",
Content = CreateCollectionView("CollectionViewFlyout")
};

var toggleButton = new Button
{
Text = "Open Flyout Menu",
FontSize = 24,
AutomationId = "FlyoutButton",
Margin = 10,
WidthRequest = 220,
HeightRequest = 50,
HorizontalOptions = LayoutOptions.Center,
BackgroundColor = Colors.Blue
};
toggleButton.Clicked += ToggleFlyoutMenu;

var detailCollectionView = CreateCollectionView("CollectionViewDetail");
detailCollectionView.Footer = toggleButton;

Detail = new ContentPage
{
Content = detailCollectionView
};

this.On<iOS>().SetUseSafeArea(true);
}

private CollectionView CreateCollectionView(string automationId)
{
return new CollectionView
{
ItemsSource = new[] { "Item 1", "Item 2", "Item 3", "Item 4" },
AutomationId = automationId,
Margin = 10,
ItemTemplate = new DataTemplate(() =>
{
var titleLabel = new Label
{
FontSize = 32,
LineBreakMode = LineBreakMode.TailTruncation
};
titleLabel.SetBinding(Label.TextProperty, new Binding("."));

var subHeaderLabel = new Label
{
FontSize = 16,
Opacity = 0.66,
LineBreakMode = LineBreakMode.TailTruncation,
Text = "subheader"
};

return new VerticalStackLayout
{
Padding = new Thickness(5),
Children = { titleLabel, subHeaderLabel }
};
})
};
}

private void ToggleFlyoutMenu(object sender, EventArgs e) => IsPresented = !IsPresented;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#if IOS //iOS only support the SetUseSafeArea
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

internal class Issue29170 : _IssuesUITest
{
public Issue29170(TestDevice device) : base(device) { }

public override string Issue => "First Item in CollectionView Overlaps in FlyoutPage.Flyout on iOS";

[Test]
[Category(UITestCategories.FlyoutPage)]
public void CollectionViewFirstItemShouldNotOverlapWithSafeAreaInFlyoutMenu()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test is failing on iOS:

CollectionView Y position should be at least 69 to avoid overlapping with safe area
Assert.That(App.WaitForElement("CollectionView").GetRect().Y, Is.GreaterThanOrEqualTo(69))
Expected: greater than or equal to 69
But was:  54

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jsuarezruiz , now the test case has been updated with proper assert conditions that correctly verify safe area respecting behavior on iOS devices.

{
// Use 44 (status bar height) as conservative minimum instead of device-specific value
// This prevents test flakiness across different simulators and iOS versions
const int MinimumSafeAreaHeight = 44;

Assert.That(App.WaitForElement("CollectionViewDetail").GetRect().Y, Is.GreaterThanOrEqualTo(MinimumSafeAreaHeight),
$"CollectionView Y position should be at least {MinimumSafeAreaHeight} to avoid overlapping with safe area");
App.WaitForElement("FlyoutButton").Tap();
Assert.That(App.WaitForElement("CollectionViewFlyout").GetRect().Y, Is.GreaterThanOrEqualTo(MinimumSafeAreaHeight),
$"CollectionView Y position should be at least {MinimumSafeAreaHeight} to avoid overlapping with safe area");
}
}
#endif
Loading