diff --git a/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj b/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj index 58d2c6ca3ed9..074098a2858c 100644 --- a/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj +++ b/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj @@ -10,6 +10,7 @@ maccatalyst-x64 maccatalyst-arm64 true + Maui.Controls.Sample diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue24246.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue24246.xaml new file mode 100644 index 000000000000..400553f8c34b --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue24246.xaml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue24246.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue24246.xaml.cs new file mode 100644 index 000000000000..6a2fa194133a --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue24246.xaml.cs @@ -0,0 +1,11 @@ +namespace Maui.Controls.Sample.Issues; + + +[Issue(IssueTracker.Github, 24246, "SafeArea arrange insets are currently insetting based on an incorrect Bounds", PlatformAffected.iOS)] +public partial class Issue24246 : ContentPage +{ + public Issue24246() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24246.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24246.cs new file mode 100644 index 000000000000..d23493671acd --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24246.cs @@ -0,0 +1,26 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues +{ + public class Issue24246 : _IssuesUITest + { + public Issue24246(TestDevice testDevice) : base(testDevice) + { + } + + public override string Issue => "SafeArea arrange insets are currently insetting based on an incorrect Bounds"; + + [Test] + [Category(UITestCategories.Layout)] + public void TapThenDoubleTap() + { + App.WaitForElement("entry"); + App.EnterText("entry", "Hello, World!"); + + var result = App.WaitForElement("entry").GetText(); + Assert.That(result, Is.EqualTo("Hello, World!")); + } + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/iOS/MauiView.cs b/src/Core/src/Platform/iOS/MauiView.cs index e125a46f5586..afdf1f86b097 100644 --- a/src/Core/src/Platform/iOS/MauiView.cs +++ b/src/Core/src/Platform/iOS/MauiView.cs @@ -25,9 +25,16 @@ public IView? View bool RespondsToSafeArea() { + if (View is not ISafeAreaView sav || sav.IgnoreSafeArea) + { + return false; + } + if (_respondsToSafeArea.HasValue) return _respondsToSafeArea.Value; + return (bool)(_respondsToSafeArea = RespondsToSelector(new Selector("safeAreaInsets"))); + } protected CGRect AdjustForSafeArea(CGRect bounds) @@ -35,7 +42,7 @@ protected CGRect AdjustForSafeArea(CGRect bounds) if (KeyboardAutoManagerScroll.ShouldIgnoreSafeAreaAdjustment) KeyboardAutoManagerScroll.ShouldScrollAgain = true; - if (View is not ISafeAreaView sav || sav.IgnoreSafeArea || !RespondsToSafeArea()) + if (!RespondsToSafeArea()) { return bounds; } @@ -88,6 +95,12 @@ Size CrossPlatformArrange(Rect bounds) return CrossPlatformLayout?.CrossPlatformArrange(bounds) ?? Size.Zero; } + // SizeThatFits does not take into account the constraints set on the view. + // For example, if the user has set a width and height on this view, those constraints + // will not be reflected in the value returned from this method. This method purely returns + // a measure based on the size that is passed in. + // The constraints are all applied by ViewHandlerExtensions.GetDesiredSizeFromHandler + // after it calls this method. public override CGSize SizeThatFits(CGSize size) { if (_crossPlatformLayoutReference == null) @@ -102,6 +115,23 @@ public override CGSize SizeThatFits(CGSize size) CacheMeasureConstraints(widthConstraint, heightConstraint); + if (RespondsToSafeArea()) + { + // During the LayoutSubViews pass, we adjust the Bounds of this view for the safe area and then pass the adjusted result to CrossPlatformArrange. + // The CrossPlatformMeasure call does not include the safe area, so we need to add it here to ensure the returned size is correct. + // + // For example, if this is a layout with an Entry of height 20, CrossPlatformMeasure will return a height of 20. + // This means the bounds will be set to a height of 20, causing AdjustForSafeArea(Bounds) to return a negative bounds once it has + // subtracted the safe area insets. Therefore, we need to add the safe area insets to the CrossPlatformMeasure result to ensure correct arrangement. + var widthSafeAreaOffset = SafeAreaInsets.Left + SafeAreaInsets.Right; + var heightSafeAreaOffset = SafeAreaInsets.Top + SafeAreaInsets.Bottom; + + var width = double.Clamp(crossPlatformSize.Width + widthSafeAreaOffset, 0, widthConstraint); + var height = double.Clamp(crossPlatformSize.Height + heightSafeAreaOffset, 0, heightConstraint); + + return new CGSize(width, height); + } + return crossPlatformSize.ToCGSize(); }