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..18c675bd98a7 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue24246.xaml @@ -0,0 +1,10 @@ + + + + + \ 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..4e669824eb61 --- /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/MauiScrollView.cs b/src/Core/src/Platform/iOS/MauiScrollView.cs index 00c1ca3d55e8..584d422e7156 100644 --- a/src/Core/src/Platform/iOS/MauiScrollView.cs +++ b/src/Core/src/Platform/iOS/MauiScrollView.cs @@ -9,6 +9,8 @@ public class MauiScrollView : UIScrollView, IUIViewLifeCycleEvents { public MauiScrollView() { + // By setting this to 'Never', we take full control of the content layout and manage the insets manually in our code, avoiding any automatic adjustments by the system. + ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never; } // overriding this method so it does not automatically scroll large UITextFields diff --git a/src/Core/src/Platform/iOS/MauiView.cs b/src/Core/src/Platform/iOS/MauiView.cs index 1d9b985611e7..240b17887da5 100644 --- a/src/Core/src/Platform/iOS/MauiView.cs +++ b/src/Core/src/Platform/iOS/MauiView.cs @@ -26,6 +26,11 @@ 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"))); @@ -38,7 +43,7 @@ protected CGRect AdjustForSafeArea(CGRect bounds) KeyboardAutoManagerScroll.ShouldScrollAgain = true; } - if (View is not ISafeAreaView sav || sav.IgnoreSafeArea || !RespondsToSafeArea()) + if (!RespondsToSafeArea()) { return bounds; } @@ -91,6 +96,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) @@ -105,6 +116,27 @@ public override CGSize SizeThatFits(CGSize size) CacheMeasureConstraints(widthConstraint, heightConstraint); + // If for some reason the upstream measure passes in a negative contraint + // Lets just bypass this code + if (RespondsToSafeArea() && widthConstraint >= 0 && heightConstraint >= 0) + { + // 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; + + CacheMeasureConstraints(widthConstraint + widthSafeAreaOffset, heightConstraint + heightSafeAreaOffset); + + 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(); }