diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue19214.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214.xaml
new file mode 100644
index 000000000000..1ab8c03e2a8e
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214.xaml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue19214.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214.xaml.cs
new file mode 100644
index 000000000000..92c88ae3c284
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214.xaml.cs
@@ -0,0 +1,15 @@
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Maui.Controls.Sample.Issues;
+
+[XamlCompilation(XamlCompilationOptions.Compile)]
+[Issue(IssueTracker.Github, 19214, "iOS Keyboard Scrolling ContentInset Tests", PlatformAffected.iOS)]
+public partial class Issue19214 : ContentPage
+{
+ public Issue19214()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_2.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_2.xaml
new file mode 100644
index 000000000000..6971aa1effbc
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_2.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_2.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_2.xaml.cs
new file mode 100644
index 000000000000..c62fc22993cb
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_2.xaml.cs
@@ -0,0 +1,72 @@
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Maui.Controls.Sample.Issues;
+
+[XamlCompilation(XamlCompilationOptions.Compile)]
+[Issue(IssueTracker.Github, 19214_2, "iOS Editor Cursor stays above keyboard - Top level Grid", PlatformAffected.iOS)]
+public partial class Issue19214_2 : ContentPage
+{
+ public Issue19214_2()
+ {
+ InitializeComponent();
+ }
+
+ private void Button_Clicked(object sender, EventArgs e)
+ {
+ editor.Text = string.Empty;
+ }
+
+ private void Editor_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ if (sender is Editor editor)
+ {
+ AddCursorHeightToLabel(editor);
+ }
+ }
+
+ void AddCursorHeightToLabel (Editor editor)
+ {
+#if IOS
+ var textInput = editor.Handler.PlatformView as UIKit.UITextView;
+ var selectedTextRange = textInput?.SelectedTextRange;
+ var localCursor = selectedTextRange is not null ? textInput?.GetCaretRectForPosition(selectedTextRange.Start) : null;
+
+ if (localCursor is CoreGraphics.CGRect local && textInput is not null)
+ {
+ var container = GetContainerView(textInput);
+ var cursorInContainer = container.ConvertRectFromView(local, textInput);
+ var cursorInWindow = container.ConvertRectToView(cursorInContainer, null);
+
+ CursorHeightTracker.Text = cursorInWindow.Y.ToString();
+ }
+
+ }
+
+ UIKit.UIView GetContainerView(UIKit.UIView startingPoint)
+ {
+ var rootView = FindResponder(startingPoint)?.View;
+
+ if (rootView is not null)
+ {
+ return rootView;
+ }
+
+ return null;
+ }
+
+ T FindResponder(UIKit.UIView view) where T : UIKit.UIResponder
+ {
+ var nextResponder = view as UIKit.UIResponder;
+ while (nextResponder is not null)
+ {
+ nextResponder = nextResponder.NextResponder;
+
+ if (nextResponder is T responder)
+ return responder;
+ }
+ return null;
+#endif
+ }
+}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_3.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_3.xaml
new file mode 100644
index 000000000000..69d4bbfcd7f8
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_3.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_3.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_3.xaml.cs
new file mode 100644
index 000000000000..4ff0c3238ebc
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue19214_3.xaml.cs
@@ -0,0 +1,72 @@
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Maui.Controls.Sample.Issues;
+
+[XamlCompilation(XamlCompilationOptions.Compile)]
+[Issue(IssueTracker.Github, 19214_3, "iOS Editor Cursor stays above keyboard - Top level ScrollView", PlatformAffected.iOS)]
+public partial class Issue19214_3 : ContentPage
+{
+ public Issue19214_3()
+ {
+ InitializeComponent();
+ }
+
+ private void Button_Clicked(object sender, EventArgs e)
+ {
+ editor.Text = string.Empty;
+ }
+
+ private void Editor_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ if (sender is Editor editor)
+ {
+ AddCursorHeightToLabel(editor);
+ }
+ }
+
+ void AddCursorHeightToLabel (Editor editor)
+ {
+#if IOS
+ var textInput = editor.Handler.PlatformView as UIKit.UITextView;
+ var selectedTextRange = textInput?.SelectedTextRange;
+ var localCursor = selectedTextRange is not null ? textInput?.GetCaretRectForPosition(selectedTextRange.Start) : null;
+
+ if (localCursor is CoreGraphics.CGRect local && textInput is not null)
+ {
+ var container = GetContainerView(textInput);
+ var cursorInContainer = container.ConvertRectFromView(local, textInput);
+ var cursorInWindow = container.ConvertRectToView(cursorInContainer, null);
+
+ CursorHeightTracker.Text = cursorInWindow.Y.ToString();
+ }
+
+ }
+
+ UIKit.UIView GetContainerView(UIKit.UIView startingPoint)
+ {
+ var rootView = FindResponder(startingPoint)?.View;
+
+ if (rootView is not null)
+ {
+ return rootView;
+ }
+
+ return null;
+ }
+
+ T FindResponder(UIKit.UIView view) where T : UIKit.UIResponder
+ {
+ var nextResponder = view as UIKit.UIResponder;
+ while (nextResponder is not null)
+ {
+ nextResponder = nextResponder.NextResponder;
+
+ if (nextResponder is T responder)
+ return responder;
+ }
+ return null;
+#endif
+ }
+}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue22715.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue22715.xaml
new file mode 100644
index 000000000000..055e032f167f
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue22715.xaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue22715.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue22715.xaml.cs
new file mode 100644
index 000000000000..b314ff6da302
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue22715.xaml.cs
@@ -0,0 +1,32 @@
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Maui.Controls.Sample.Issues;
+
+[XamlCompilation(XamlCompilationOptions.Compile)]
+[Issue(IssueTracker.Github, 22715, "Page should not scroll when focusing element above keyboard", PlatformAffected.iOS)]
+public partial class Issue22715 : ContentPage
+{
+ public Issue22715()
+ {
+ InitializeComponent();
+ }
+
+ private void OnPageLoaded(object sender, EventArgs e)
+ {
+ EntNumber.Focus();
+ }
+
+ void EntNumber_Focused(object sender, FocusEventArgs e)
+ {
+#if IOS
+ var entry = (Entry)sender;
+ var field = entry.Handler?.PlatformView as UIKit.UITextField;
+ if (field is not null)
+ {
+ field.TintColor = UIKit.UIColor.Clear;
+ }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue24496.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue24496.xaml
new file mode 100644
index 000000000000..e643ca067e81
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue24496.xaml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+ Test 1
+ Test 2
+ Test 3
+ Test 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue24496.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue24496.xaml.cs
new file mode 100644
index 000000000000..7cb5adf735d9
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue24496.xaml.cs
@@ -0,0 +1,28 @@
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Maui.Controls.Sample.Issues
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ [Issue(IssueTracker.Github, 24496, "Pickers scroll to bottom and new keyboard types rekick the scrolling", PlatformAffected.iOS)]
+ public partial class Issue24496 : ContentPage
+ {
+ public Issue24496()
+ {
+ InitializeComponent();
+ }
+
+ void Entry_Focused(object sender, FocusEventArgs e)
+ {
+#if IOS
+ var entry = (Entry)sender;
+ var field = entry.Handler?.PlatformView as UIKit.UITextField;
+ if (field is not null)
+ {
+ field.TintColor = UIKit.UIColor.Clear;
+ }
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19214.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19214.cs
new file mode 100644
index 000000000000..7763aa3c861a
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19214.cs
@@ -0,0 +1,118 @@
+#if IOS
+using System.Drawing;
+using NUnit.Framework;
+using NUnit.Framework.Legacy;
+using OpenQA.Selenium.Appium.Interactions;
+using OpenQA.Selenium.Appium.MultiTouch;
+using OpenQA.Selenium.Interactions;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+public class Issue19214 : _IssuesUITest
+{
+ public Issue19214(TestDevice device) : base(device) { }
+
+ public override string Issue => "iOS Keyboard Scrolling ContentInset Tests";
+
+ [Test]
+ [Category(UITestCategories.Entry)]
+ public void TestMultipleScrollViews ()
+ {
+ var app = App as AppiumApp;
+ if (app is null)
+ {
+ return;
+ }
+
+ var topRectY = app.WaitForElement("TopEntry_1").GetRect().Y;
+ var bottomRectY = app.WaitForElement("Entry2_3").GetRect().Y;
+
+ for (int i = 1; i < 4; i++)
+ {
+ var topEntry = $"TopEntry_{i}";
+ var bottomEntry = $"BottomEntry_{i}";
+ var entryTwo = $"Entry2_{i}";
+ var entryTen = $"Entry10_{i}";
+ var scrollView = $"ScrollView_{i}";
+
+ CheckInsets(app, topEntry, topEntry, bottomEntry, scrollView);
+ CheckInsets(app, entryTwo, topEntry, bottomEntry, scrollView);
+ CheckInsets(app, entryTen, topEntry, bottomEntry, scrollView, true);
+ }
+ }
+
+ void CheckInsets (AppiumApp app, string queryEntry, string topEntry, string bottomEntry, string scrollView, bool startFromBottom = false)
+ {
+ if (startFromBottom)
+ {
+ var startRect = app.WaitForElement(topEntry).GetRect();
+ ScrollScrollView(app, startRect);
+ }
+
+ var queryRect = app.WaitForElement(queryEntry).GetRect();
+ ClassicAssert.NotNull(queryRect, "Could not find the initial entry.");
+
+ app.Click(queryEntry);
+ queryRect = app.WaitForElement(queryEntry).GetRect();
+ KeyboardScrolling.CheckIfViewAboveKeyboard(app, queryEntry, false);
+
+ // Make sure we can scroll up to the top entry
+ ScrollScrollView(app, queryRect, false);
+ var topRect = app.WaitForElement(topEntry).GetRect();
+
+ ConfirmVisible (app, topRect, scrollView, topEntry, true);
+
+ // Scroll to the bottom of the ScrollView
+ ScrollScrollView(app, topRect);
+
+ // Make sure we get to the bottom of the ScrollView
+ var bottomRect = app.WaitForElement(bottomEntry).GetRect();
+ ConfirmVisible (app, bottomRect, scrollView, bottomEntry, false);
+
+ // Scroll back up and make sure we can get all the way up
+ ScrollScrollView(app, bottomRect, false);
+ topRect = app.WaitForElement(topEntry).GetRect();
+ ConfirmVisible (app, topRect, scrollView, topEntry, true);
+
+ // Hide the keyboard
+ KeyboardScrolling.HideKeyboard(app, app.Driver, false);
+ }
+
+ void ConfirmVisible (AppiumApp app, Rectangle rect, string scrollView, string entry, bool isTopField)
+ {
+ var scrollViewRect = app.WaitForElement(scrollView).GetRect();
+ KeyboardScrolling.CheckIfViewAboveKeyboard(app, entry, false);
+ // ClassicAssert.True(rect.Y > scrollViewRect.Y && rect.Bottom < scrollViewRect.Bottom, $"{entry} was not visible in {scrollView}");
+ if (isTopField)
+ {
+ ClassicAssert.Greater(rect.Y, scrollViewRect.Y, $"rect.Y: {rect.Y} was not greater than scrollViewRect.Y: {scrollViewRect.Y}");
+ }
+ else
+ {
+ ClassicAssert.Less(rect.Bottom, scrollViewRect.Bottom, $"rect.Bottom: {rect.Bottom} was not less than scrollViewRect.Bottom: {scrollViewRect.Bottom}");
+ }
+ }
+
+ void ScrollScrollView (AppiumApp app, Rectangle rect, bool scrollsDown = true)
+ {
+ var newY = scrollsDown ? rect.Y - 5000 : rect.Y + 5000;
+
+ OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch);
+ var scrollSequence = new ActionSequence(touchDevice, 0);
+ if (scrollsDown)
+ {
+ scrollSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, rect.Left - 5, rect.Y, TimeSpan.Zero));
+ }
+ else
+ {
+ scrollSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, rect.Left - 5, rect.Bottom, TimeSpan.Zero));
+ }
+ scrollSequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact));
+ scrollSequence.AddAction(touchDevice.CreatePause(TimeSpan.FromMilliseconds(500)));
+ scrollSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, rect.Left - 5, newY, TimeSpan.FromMilliseconds(250)));
+ scrollSequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact));
+ app.Driver.PerformActions([scrollSequence]);
+ }
+}
+#endif
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19214_2.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19214_2.cs
new file mode 100644
index 000000000000..9e9fb0c7d1c2
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19214_2.cs
@@ -0,0 +1,77 @@
+#if IOS
+using System.Drawing;
+using NUnit.Framework;
+using NUnit.Framework.Legacy;
+using OpenQA.Selenium.Appium.Interactions;
+using OpenQA.Selenium.Appium.MultiTouch;
+using OpenQA.Selenium.Interactions;
+using UITest.Appium;
+using UITest.Core;
+using System.Text;
+using OpenQA.Selenium;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+public class Issue19214_2 : _IssuesUITest
+{
+ public Issue19214_2(TestDevice device) : base(device) { }
+
+ public override string Issue => "iOS Editor Cursor stays above keyboard - Top level Grid";
+
+ [Test]
+ [Category(UITestCategories.Entry)]
+ public void KeepEditorCursorAboveKeyboardInGrid ()
+ {
+ var app = App as AppiumApp;
+ if (app is null)
+ {
+ return;
+ }
+
+ var editorRect = app.WaitForElement("IssueEditor").GetRect();
+ app.Click("IssueEditor");
+
+ var sb = new StringBuilder();
+ for (int i = 1; i <= 30; i++)
+ {
+ sb.Append($"\n{i}");
+ }
+
+ app.EnterText("IssueEditor", sb.ToString());
+
+ var keyboardLocation = KeyboardScrolling.FindiOSKeyboardLocation(app.Driver);
+
+ var cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
+
+ var cursorHeight1 = Convert.ToDouble(cursorLabel);
+ KeyboardScrolling.HideKeyboard(app, app.Driver, true);
+
+ // Click a low spot on the editor
+ var lowSpotY = editorRect.Y + editorRect.Height - 100;
+ app.TapCoordinates(editorRect.X + 10, lowSpotY);
+
+ app.EnterText("IssueEditor", "A");
+
+ cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
+ var cursorHeight2 = Convert.ToDouble(cursorLabel);
+
+ app.EnterText("IssueEditor", sb.ToString());
+
+ cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
+ var cursorHeight3 = Convert.ToDouble(cursorLabel);
+
+ if (keyboardLocation is Point keyboardPoint)
+ {
+ ClassicAssert.True(cursorHeight1 > 0);
+ ClassicAssert.True(cursorHeight2 > 0);
+ ClassicAssert.True(cursorHeight3 > 0);
+ ClassicAssert.True(cursorHeight1 < keyboardPoint.Y);
+ ClassicAssert.True(cursorHeight2 < keyboardPoint.Y);
+ ClassicAssert.True(cursorHeight3 < keyboardPoint.Y);
+ }
+ else
+ {
+ ClassicAssert.Fail("keyboardLocation is null");
+ }
+ }
+}
+#endif
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19214_3.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19214_3.cs
new file mode 100644
index 000000000000..bf91f521a237
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19214_3.cs
@@ -0,0 +1,77 @@
+#if IOS
+using System.Drawing;
+using NUnit.Framework;
+using NUnit.Framework.Legacy;
+using OpenQA.Selenium.Appium.Interactions;
+using OpenQA.Selenium.Appium.MultiTouch;
+using OpenQA.Selenium.Interactions;
+using UITest.Appium;
+using UITest.Core;
+using System.Text;
+using OpenQA.Selenium;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+public class Issue19214_3 : _IssuesUITest
+{
+ public Issue19214_3(TestDevice device) : base(device) { }
+
+ public override string Issue => "iOS Editor Cursor stays above keyboard - Top level ScrollView";
+
+ [Test]
+ [Category(UITestCategories.Entry)]
+ public void KeepEditorCursorAboveKeyboardInScrollView ()
+ {
+ var app = App as AppiumApp;
+ if (app is null)
+ {
+ return;
+ }
+
+ var editorRect = app.WaitForElement("IssueEditor").GetRect();
+ app.Click("IssueEditor");
+
+ var sb = new StringBuilder();
+ for (int i = 1; i <= 30; i++)
+ {
+ sb.Append($"\n{i}");
+ }
+
+ app.EnterText("IssueEditor", sb.ToString());
+
+ var keyboardLocation = KeyboardScrolling.FindiOSKeyboardLocation(app.Driver);
+
+ var cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
+
+ var cursorHeight1 = Convert.ToDouble(cursorLabel);
+ KeyboardScrolling.HideKeyboard(app, app.Driver, true);
+
+ // Click a low spot on the editor
+ var lowSpotY = editorRect.Y + editorRect.Height - 100;
+ app.TapCoordinates(editorRect.X + 10, lowSpotY);
+
+ app.EnterText("IssueEditor", "A");
+
+ cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
+ var cursorHeight2 = Convert.ToDouble(cursorLabel);
+
+ app.EnterText("IssueEditor", sb.ToString());
+
+ cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
+ var cursorHeight3 = Convert.ToDouble(cursorLabel);
+
+ if (keyboardLocation is Point keyboardPoint)
+ {
+ ClassicAssert.True(cursorHeight1 > 0);
+ ClassicAssert.True(cursorHeight2 > 0);
+ ClassicAssert.True(cursorHeight3 > 0);
+ ClassicAssert.True(cursorHeight1 < keyboardPoint.Y);
+ ClassicAssert.True(cursorHeight2 < keyboardPoint.Y);
+ ClassicAssert.True(cursorHeight3 < keyboardPoint.Y);
+ }
+ else
+ {
+ ClassicAssert.Fail("keyboardLocation is null");
+ }
+ }
+}
+#endif
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19956.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19956.cs
index 1634af1a067e..a95bd1503fdd 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19956.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19956.cs
@@ -55,20 +55,31 @@ public void BottomInsetsSetCorrectly()
{
var app = App as AppiumApp;
if (app is null)
+ {
return;
-
- App.Tap("Entry5");
- ScrollToBottom(app);
- CheckForBottomEntry(app);
- KeyboardScrolling.NextiOSKeyboardPress(app.Driver);
-
- App.Tap("Entry10");
- ScrollToBottom(app);
- CheckForBottomEntry(app);
- KeyboardScrolling.NextiOSKeyboardPress(app.Driver);
-
- ScrollToBottom(app);
- CheckForBottomEntry(app);
+ }
+
+ try
+ {
+ App.Tap("Entry5");
+ ScrollToBottom(app);
+ CheckForBottomEntry(app);
+ KeyboardScrolling.NextiOSKeyboardPress(app.Driver);
+
+ App.Tap("Entry10");
+ ScrollToBottom(app);
+ CheckForBottomEntry(app);
+ KeyboardScrolling.NextiOSKeyboardPress(app.Driver);
+
+ ScrollToBottom(app);
+ CheckForBottomEntry(app);
+ }
+ finally
+ {
+ //Reset the app so other UITest is in a clean state
+ Reset();
+ FixtureSetup();
+ }
}
static void ScrollToBottom(AppiumApp app)
@@ -92,4 +103,4 @@ void CheckForBottomEntry (AppiumApp app)
ClassicAssert.Less(bottomEntryRect.Bottom, keyboardPosition!.Value.Y);
}
}
-#endif
\ No newline at end of file
+#endif
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22715.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22715.cs
new file mode 100644
index 000000000000..4049f8893d1a
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22715.cs
@@ -0,0 +1,27 @@
+#if IOS
+using System.Drawing;
+using NUnit.Framework;
+using NUnit.Framework.Legacy;
+using OpenQA.Selenium.Appium.Interactions;
+using OpenQA.Selenium.Appium.MultiTouch;
+using OpenQA.Selenium.Interactions;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+public class Issue22715 : _IssuesUITest
+{
+ public Issue22715(TestDevice device) : base(device) { }
+
+ public override string Issue => "Page should not scroll when focusing element above keyboard";
+
+ [Test]
+ [Category(UITestCategories.Entry)]
+ public void PageShouldNotScroll ()
+ {
+ App.WaitForElement("EntNumber").GetRect();
+ App.WaitForElement("TopLabel").GetRect();
+ VerifyScreenshot();
+ }
+}
+#endif
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24496.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24496.cs
new file mode 100644
index 000000000000..d04a40983e6d
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24496.cs
@@ -0,0 +1,28 @@
+#if IOS
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+ public class Issue24496 : _IssuesUITest
+ {
+ public Issue24496(TestDevice testDevice) : base(testDevice)
+ {
+ }
+
+ public override string Issue => "Pickers scroll to bottom and new keyboard types rekick the scrolling";
+
+ [Test]
+ [Category(UITestCategories.Entry)]
+ public void PickerNewKeyboardIsAboveKeyboard()
+ {
+ App.WaitForElement("Picker6");
+ App.Tap("Picker6");
+ VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_Picker6");
+ App.Tap("Entry7");
+ VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_Entry7");
+ }
+ }
+}
+#endif
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/KeyboardScrolling.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/KeyboardScrolling.cs
index d4eefd9ab61d..c64838b99d26 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/KeyboardScrolling.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/KeyboardScrolling.cs
@@ -71,7 +71,7 @@ static void ClickText(IApp app, string marked, bool isEditor, out bool didReachE
}
// will return a bool showing if the view is visible
- static bool CheckIfViewAboveKeyboard(IApp app, string marked, bool isEditor)
+ internal static bool CheckIfViewAboveKeyboard(IApp app, string marked, bool isEditor)
{
var views = app.WaitForElement(marked);
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PageShouldNotScroll.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PageShouldNotScroll.png
new file mode 100644
index 000000000000..3b387e1c07ff
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PageShouldNotScroll.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Entry7.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Entry7.png
new file mode 100644
index 000000000000..1cf45c8b162d
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Entry7.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Picker6.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Picker6.png
new file mode 100644
index 000000000000..6c217454d81c
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Picker6.png differ
diff --git a/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs b/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs
index 729bd2cf66c8..989663895b86 100644
--- a/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs
+++ b/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs
@@ -7,7 +7,6 @@
using System;
using System.Text.RegularExpressions;
-using System.Threading;
using System.Threading.Tasks;
using CoreGraphics;
using Foundation;
@@ -30,11 +29,8 @@ public static class KeyboardAutoManagerScroll
static UIView? View;
static UIView? ContainerView;
static CGRect? CursorRect;
- static CGRect? StartingContainerViewFrame;
internal static bool IsKeyboardShowing;
static int TextViewDistanceFromBottom = 20;
- static int TextViewDistanceFromTop = 5;
- static int DebounceCount;
static NSObject? WillShowToken;
static NSObject? WillHideToken;
static NSObject? DidHideToken;
@@ -55,7 +51,9 @@ public static class KeyboardAutoManagerScroll
public static void Connect()
{
if (TextFieldToken is not null)
+ {
return;
+ }
TextFieldToken = NSNotificationCenter.DefaultCenter.AddObserver(new NSString("UITextFieldTextDidBeginEditingNotification"), DidUITextBeginEditing);
@@ -109,7 +107,7 @@ public static void Disconnect()
IsKeyboardAutoScrollHandling = false;
}
- static async void DidUITextBeginEditing(NSNotification notification)
+ static void DidUITextBeginEditing(NSNotification notification)
{
IsKeyboardAutoScrollHandling = true;
@@ -127,12 +125,7 @@ static async void DidUITextBeginEditing(NSNotification notification)
ContainerView = View.GetContainerView();
- // Grab the starting position of the ContainerView so we can track if
- // there is any external scrolling going on
- if (ContainerView is not null)
- StartingContainerViewFrame = ContainerView.ConvertRectToView(ContainerView.Bounds, null);
-
- await AdjustPositionDebounce();
+ AdjustPositionDebounce().FireAndForget();
}
}
@@ -146,30 +139,42 @@ static async void DidUITextBeginEditing(NSNotification notification)
internal static CGRect? FindCursorPosition()
{
var localCursor = FindLocalCursorPosition();
- if (localCursor is CGRect local)
- return View?.ConvertRectToView(local, null);
+ if (localCursor is CGRect local && ContainerView is not null)
+ {
+ var cursorInContainer = ContainerView.ConvertRectFromView(local, View);
+ var cursorInWindow = ContainerView.ConvertRectToView(cursorInContainer, null);
+ return cursorInWindow;
+ }
return null;
}
- static async void WillKeyboardShow(NSNotification notification)
+ static void WillKeyboardShow(NSNotification notification)
{
var userInfo = notification.UserInfo;
+ var oldKeyboardFrame = KeyboardFrame;
if (userInfo is not null)
{
var frameSize = userInfo.FindValue("UIKeyboardFrameEndUserInfoKey");
var frameSizeRect = DescriptionToCGRect(frameSize?.Description);
if (frameSizeRect is not null)
+ {
KeyboardFrame = (CGRect)frameSizeRect;
+ }
userInfo.SetAnimationDuration();
}
if (!IsKeyboardShowing)
{
- await AdjustPositionDebounce();
IsKeyboardShowing = true;
+ AdjustPositionDebounce().FireAndForget();
+ }
+ else if (oldKeyboardFrame != KeyboardFrame && IsKeyboardShowing)
+ {
+ // this could be the case if the keyboard is already showing but type of keyboard changes
+ AdjustPositionDebounce().FireAndForget();
}
}
@@ -178,10 +183,14 @@ static void WillHideKeyboard(NSNotification notification)
notification.UserInfo?.SetAnimationDuration();
if (LastScrollView is not null)
+ {
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, AnimateHidingKeyboard, () => { });
+ }
if (IsKeyboardShowing)
+ {
RestorePosition();
+ }
IsKeyboardShowing = false;
View = null;
@@ -212,7 +221,9 @@ static void SetAnimationDuration(this NSDictionary dict)
var durationNum = (NSNumber)NSObject.FromObject(durationObj);
var num = (double)durationNum;
if (num != 0)
+ {
AnimationDuration = num;
+ }
}
static void AnimateHidingKeyboard()
@@ -236,9 +247,13 @@ static void AnimateHidingKeyboard()
if (!superScrollView.ContentOffset.Equals(newContentOffset))
{
if (View?.Superview is UIStackView)
+ {
superScrollView.SetContentOffset(newContentOffset, UIView.AnimationsEnabled);
+ }
else
+ {
superScrollView.ContentOffset = newContentOffset;
+ }
}
}
superScrollView = superScrollView.FindResponder();
@@ -252,7 +267,9 @@ static void AnimateHidingKeyboard()
// example of passed in description: "NSRect: {{0, 586}, {430, 346}}"
if (description is null)
+ {
return null;
+ }
// remove everything except for numbers and commas
var temp = Regex.Replace(description, @"[^0-9,]", "");
@@ -274,24 +291,23 @@ static void AnimateHidingKeyboard()
// all the fields are updated before calling AdjustPostition()
internal static async Task AdjustPositionDebounce()
{
- Interlocked.Increment(ref DebounceCount);
-
- var entranceCount = DebounceCount;
-
- // If we are going to a new view that has an InputAccessoryView
- // while we have the keyboard up, we need a delay to recalculate
- // the height of the InputAccessoryView
- if (IsKeyboardShowing && View?.InputAccessoryView is not null)
- await Task.Delay(30);
-
- if (entranceCount == DebounceCount)
+ if (IsKeyboardShowing)
{
+ // If we are going to a new view that has an InputAccessoryView
+ // while we have the keyboard up, we need a delay to recalculate
+ // the height of the InputAccessoryView
+ if (View?.InputAccessoryView is not null)
+ {
+ await Task.Delay(30);
+ }
AdjustPosition();
// See if the layout requests to scroll again after our initial scroll
await Task.Delay(5);
if (ShouldScrollAgain)
+ {
AdjustPosition();
+ }
}
}
@@ -307,7 +323,9 @@ internal static void AdjustPosition()
}
if (TopViewBeginOrigin == InvalidPoint)
+ {
TopViewBeginOrigin = new CGPoint(ContainerView.Frame.X, ContainerView.Frame.Y);
+ }
var rootViewOrigin = new CGPoint(ContainerView.Frame.GetMinX(), ContainerView.Frame.GetMinY());
var window = ContainerView.Window;
@@ -344,12 +362,10 @@ internal static void AdjustPosition()
navigationBarAreaHeight = statusBarHeight;
}
- var topLayoutGuide = Math.Max(navigationBarAreaHeight, ContainerView.LayoutMargins.Top) + TextViewDistanceFromTop;
+ var topLayoutGuide = Math.Max(navigationBarAreaHeight, ContainerView.LayoutMargins.Top);
// calculate the cursor rect
- var localCursor = FindLocalCursorPosition();
- if (localCursor is CGRect local)
- CursorRect = View.ConvertRectToView(local, null);
+ CursorRect = FindCursorPosition();
if (CursorRect is null)
{
@@ -357,70 +373,93 @@ internal static void AdjustPosition()
return;
}
- var viewRectInWindow = View.ConvertRectToView(View.Bounds, window);
+ var cursorRect = (CGRect)CursorRect;
- // give a small offset of 20 plus the cursor.Height for the distance
- // between the selected text and the keyboard
- TextViewDistanceFromBottom = ((int?)localCursor?.Height ?? 0) + 20;
+ var viewRectInContainer = ContainerView.ConvertRectFromView(View.Frame, View.Superview);
+ var viewRectInWindow = ContainerView.ConvertRectToView(viewRectInContainer, null);
+
+ // since the cursorRect does not have a height for Pickers, we can assign the height of the picker as the cursor height
+ if (cursorRect.Height == 0)
+ {
+ cursorRect.Height = View.Bounds.Height;
+ }
var keyboardYPosition = window.Frame.Height - kbSize.Height - TextViewDistanceFromBottom;
// readjust contentInset when the textView height is too large for the screen
var rootSuperViewFrameInWindow = window.Frame;
if (ContainerView.Superview is UIView v)
+ {
rootSuperViewFrameInWindow = v.ConvertRectToView(v.Bounds, window);
-
- var cursorRect = (CGRect)CursorRect;
+ }
nfloat cursorNotInViewScroll = 0;
nfloat move = 0;
bool cursorTooHigh = false;
bool cursorTooLow = false;
- // Find the next parent ScrollView that is scrollable
- var superView = View.FindResponder();
+ // Find the next parent ScrollView that is scrollable or use the current View if it is a ScrollView
+ var superView = View.FindResponder() ?? View as UIScrollView;
var superScrollView = FindParentScroll(superView);
CGRect? superScrollViewRect = null;
var topBoundary = topLayoutGuide;
var bottomBoundary = (double)keyboardYPosition;
- if (superScrollView is not null){
- superScrollViewRect = superScrollView.ConvertRectToView(superScrollView.Bounds, window);
- topBoundary = Math.Max(topBoundary, superScrollViewRect.Value.Top + TextViewDistanceFromTop);
- bottomBoundary = Math.Min(bottomBoundary, superScrollViewRect.Value.Bottom - TextViewDistanceFromBottom);
+ if (superScrollView is not null)
+ {
+ var superScrollInContainer = ContainerView.ConvertRectFromView(superScrollView.Frame, superScrollView.Superview);
+ superScrollViewRect = ContainerView.ConvertRectToView(superScrollInContainer, null);
+ topBoundary = Math.Max(topBoundary, superScrollViewRect.Value.Top);
+ var superScrollViewBottom = superScrollViewRect.Value.Bottom - TextViewDistanceFromBottom;
+
+ // if the superScrollView is a small editor, it may not make sense to scroll the entire screen if cursor is visible
+ if (superScrollView is UITextView && superScrollViewRect.Value.Bottom - TextViewDistanceFromBottom < cursorRect.Bottom)
+ {
+ superScrollViewBottom = superScrollViewRect.Value.Bottom;
+ }
+
+ bottomBoundary = Math.Min(bottomBoundary, superScrollViewBottom);
}
+ bool forceSetContentInsets = true;
+
// scenario where we go into an editor with the "Next" keyboard button,
// but the cursor location on the editor is scrolled below the visible section
- if (View is UITextView && cursorRect.Y >= viewRectInWindow.GetMaxY())
+ if (View is UITextView && IsKeyboardShowing && cursorRect.Bottom >= viewRectInWindow.GetMaxY())
{
- cursorNotInViewScroll = viewRectInWindow.GetMaxY() - cursorRect.GetMaxY();
- move = cursorRect.Y - (nfloat)bottomBoundary + cursorNotInViewScroll;
- cursorTooLow = true;
+ move = viewRectInWindow.Bottom - (nfloat)bottomBoundary;
}
// scenario where we go into an editor with the "Next" keyboard button,
// but the cursor location on the editor is scrolled above the visible section
- else if (View is UITextView && cursorRect.Y < viewRectInWindow.GetMinY())
+ else if (View is UITextView && IsKeyboardShowing && cursorRect.Y < viewRectInWindow.GetMinY())
{
- cursorNotInViewScroll = viewRectInWindow.GetMinY() - cursorRect.Y;
- move = cursorRect.Y - (nfloat)bottomBoundary + cursorNotInViewScroll;
- cursorTooHigh = true;
+ move = viewRectInWindow.Top - (nfloat)bottomBoundary;
// no need to move the screen down if we can already see the view
if (move < 0)
+ {
move = 0;
+ }
}
- else if (cursorRect.Y >= topBoundary && cursorRect.Y < bottomBoundary)
- return;
-
- else if (cursorRect.Y > bottomBoundary)
- move = cursorRect.Y - (nfloat)bottomBoundary;
+ else if (cursorRect.Bottom > bottomBoundary && cursorRect.Y > topBoundary)
+ {
+ move = cursorRect.Bottom - (nfloat)bottomBoundary;
+ }
- else if (cursorRect.Y <= topBoundary)
+ else if (cursorRect.Y <= topBoundary && cursorRect.Bottom <= bottomBoundary)
+ {
move = cursorRect.Y - (nfloat)topBoundary;
+ }
+
+ else if (cursorRect.Y <= topBoundary && cursorRect.Bottom >= bottomBoundary)
+ {
+ cursorNotInViewScroll = viewRectInWindow.GetMinY() - cursorRect.Y;
+ move = cursorRect.Bottom - (nfloat)bottomBoundary - cursorNotInViewScroll;
+ cursorTooHigh = true;
+ }
// This is the case when the keyboard is already showing and we click another editor/entry
if (LastScrollView is not null)
@@ -429,14 +468,20 @@ internal static void AdjustPosition()
if (superScrollView is null)
{
if (LastScrollView.ContentInset != StartingContentInsets)
+ {
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, AnimateStartingLastScrollView, () => { });
+ }
if (!LastScrollView.ContentOffset.Equals(StartingContentOffset))
{
if (View.FindResponder() is UIStackView)
+ {
LastScrollView.SetContentOffset(StartingContentOffset, UIView.AnimationsEnabled);
+ }
else
+ {
LastScrollView.ContentOffset = StartingContentOffset;
+ }
}
StartingContentInsets = new UIEdgeInsets();
@@ -462,13 +507,28 @@ internal static void AdjustPosition()
var lastView = View;
superScrollView = LastScrollView;
nfloat innerScrollValue = 0;
+ nfloat tempMove = 0;
while (superScrollView is not null)
{
var shouldContinue = false;
- if (move > 0)
+ // if we have an innerScrollValue, let's move with this value first and then do the move
+ if (cursorNotInViewScroll != 0)
+ {
+ tempMove = move;
+ move = cursorNotInViewScroll;
+ shouldContinue = true;
+ }
+
+ else if (move > 0 || tempMove > 0)
+ {
+ if (move == 0)
+ {
+ move = tempMove;
+ }
shouldContinue = move > -superScrollView.ContentOffset.Y - superScrollView.ContentInset.Top;
+ }
else if (superScrollView.FindResponder() is UITableView tableView)
{
@@ -510,27 +570,34 @@ internal static void AdjustPosition()
{
shouldContinue = !(innerScrollValue == 0
&& cursorRect.Y + cursorNotInViewScroll >= topBoundary
- && cursorRect.Y + cursorNotInViewScroll <= bottomBoundary);
+ && cursorRect.Bottom + cursorNotInViewScroll <= bottomBoundary);
if (cursorRect.Y - innerScrollValue < topBoundary && !cursorTooHigh)
+ {
move = cursorRect.Y - innerScrollValue - (nfloat)topBoundary;
+ }
else if (cursorRect.Y - innerScrollValue > bottomBoundary && !cursorTooLow)
+ {
move = cursorRect.Y - innerScrollValue - (nfloat)bottomBoundary;
+ }
}
// Go up the hierarchy and look for other scrollViews until we reach the UIWindow
if (shouldContinue)
{
+ forceSetContentInsets = false;
+
var tempScrollView = superScrollView.FindResponder();
var nextScrollView = FindParentScroll(tempScrollView);
- // if PrefersLargeTitles is true, we may need additional logic to
- // handle the collapsable navbar
+ // if PrefersLargeTitles is true, we may need additional logic to handle the collapsable navbar
var navController = View?.FindResponder();
var prefersLargeTitles = navController?.NavigationBar.PrefersLargeTitles ?? false;
if (prefersLargeTitles)
+ {
move = AdjustForLargeTitles(move, superScrollView, navController!);
+ }
var origContentOffsetY = superScrollView.ContentOffset.Y;
var shouldOffsetY = superScrollView.ContentOffset.Y - Math.Min(superScrollView.ContentOffset.Y, -move);
@@ -543,7 +610,8 @@ internal static void AdjustPosition()
if ((!superScrollView.ContentOffset.Equals(newContentOffset) || innerScrollValue != 0) && superScrollViewRect is not null)
{
- if (nextScrollView is null && superScrollViewRect.Value.Y < bottomBoundary)
+ if ((nextScrollView is null && superScrollViewRect.Value.Y + cursorRect.Height + TextViewDistanceFromBottom < bottomBoundary) ||
+ cursorNotInViewScroll != 0)
{
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () =>
{
@@ -552,9 +620,13 @@ internal static void AdjustPosition()
ScrolledView = superScrollView;
if (View?.FindResponder() is not null)
+ {
superScrollView.SetContentOffset(newContentOffset, UIView.AnimationsEnabled);
+ }
else
+ {
superScrollView.ContentOffset = newContentOffset;
+ }
}, () => { });
// after this scroll finishes, there is an edge case where if we have Large Titles,
@@ -563,7 +635,9 @@ internal static void AdjustPosition()
var amountNotScrolled = requestedMove - actualScrolledAmount;
if (prefersLargeTitles && amountNotScrolled > 1)
+ {
ShouldScrollAgain = true;
+ }
}
else
@@ -573,8 +647,16 @@ internal static void AdjustPosition()
}
}
- lastView = superScrollView;
- superScrollView = nextScrollView;
+ // if we needed to scroll for cursorNotInViewScroll first, use the same superScrollView and handle the move now
+ if (cursorNotInViewScroll != 0)
+ {
+ cursorNotInViewScroll = 0;
+ }
+ else
+ {
+ lastView = superScrollView;
+ superScrollView = nextScrollView;
+ }
}
else
@@ -587,26 +669,24 @@ internal static void AdjustPosition()
move += innerScrollValue;
- // ContentInset logic
- if (ScrolledView is not null)
+ // Adjust the parent's ContentInset.Bottom so we can still scroll to the top with the keyboard showing
+ if (forceSetContentInsets && superScrollView is not null)
{
- var bottomInset = kbSize.Height;
- var bottomScrollIndicatorInset = bottomInset - TextViewDistanceFromBottom;
-
- bottomInset = nfloat.Max(StartingContentInsets.Bottom, bottomInset);
- bottomScrollIndicatorInset = nfloat.Max(StartingScrollIndicatorInsets.Bottom, bottomScrollIndicatorInset);
-
- if (OperatingSystem.IsIOSVersionAtLeast(11, 0))
+ ApplyContentInset(superScrollView, LastScrollView, false, false);
+ // if our View is an editor, we can adjust the ContentInset.Bottom so that the text cursor will stay above the keyboard
+ if (superScrollView != View && View is UITextView textView)
{
- bottomInset -= ScrolledView.SafeAreaInsets.Bottom;
- bottomScrollIndicatorInset -= ScrolledView.SafeAreaInsets.Bottom;
+ ApplyContentInset(textView, textView, false, true);
+ }
+ }
+ else
+ {
+ ApplyContentInset (ScrolledView, LastScrollView, true, false);
+ // if our View is an editor, we can adjust the ContentInset.Bottom so that the text cursor will stay above the keyboard
+ if (ScrolledView != View && View is UITextView textView)
+ {
+ ApplyContentInset(textView, textView, true, true);
}
-
- var movedInsets = ScrolledView.ContentInset;
- movedInsets.Bottom = bottomInset;
-
- if (LastScrollView.ContentInset != movedInsets)
- UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateInset(ScrolledView, movedInsets, bottomScrollIndicatorInset), () => { });
}
}
@@ -622,6 +702,13 @@ internal static void AdjustPosition()
rect.Y = rootViewOrigin.Y;
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateRootView(rect), () => { });
+
+ // this is the scenario where there is a scrollview, but the whole scrollview is below
+ // where the keyboard will be. We need to scroll the ContainerView and add ContentInsets to the scrollview.
+ if (LastScrollView is not null)
+ {
+ ApplyContentInset(LastScrollView, LastScrollView, false, false);
+ }
}
}
@@ -643,15 +730,21 @@ internal static void AdjustPosition()
static void AnimateInset(UIScrollView? scrollView, UIEdgeInsets movedInsets, nfloat bottomScrollIndicatorInset)
{
if (scrollView is null)
+ {
return;
+ }
scrollView.ContentInset = movedInsets;
UIEdgeInsets newscrollIndicatorInset;
if (OperatingSystem.IsIOSVersionAtLeast(11, 0))
+ {
newscrollIndicatorInset = scrollView.VerticalScrollIndicatorInsets;
+ }
else
+ {
newscrollIndicatorInset = scrollView.ScrollIndicatorInsets;
+ }
newscrollIndicatorInset.Bottom = bottomScrollIndicatorInset;
scrollView.ScrollIndicatorInsets = newscrollIndicatorInset;
@@ -669,15 +762,76 @@ static void AnimateStartingLastScrollView()
static void AnimateRootView(CGRect rect)
{
if (ContainerView is not null)
+ {
ContainerView.Frame = rect;
+ }
+ }
+
+ // Adjusts the ContentInset of our view that Scrolled so that we can still scroll to the top and bottom with the keyboard showing.
+ static void ApplyContentInset(UIScrollView? scrolledView, UIScrollView? lastScrollView, bool didMove, bool isInnerEditor)
+ {
+ if (scrolledView is null || lastScrollView is null || ContainerView is null)
+ {
+ return;
+ }
+
+ var frameInContainer = ContainerView.ConvertRectFromView(scrolledView.Frame, scrolledView.Superview);
+ var frameInWindow = ContainerView.ConvertRectToView(frameInContainer, null);
+
+ var keyboardIntersect = CGRect.Intersect(KeyboardFrame, frameInWindow);
+
+ var bottomInset = keyboardIntersect.Height;
+
+ // For new lines in an editor, we want the cursor to stay right above the keyboard.
+ // When adding contentInsets for a scrollview, it is nice to have a little extra padding.
+ if (scrolledView is not UITextView && keyboardIntersect.Height > 0)
+ {
+ bottomInset += TextViewDistanceFromBottom;
+ }
+
+ var bottomScrollIndicatorInset = bottomInset;
+
+ bottomInset = nfloat.Max(StartingContentInsets.Bottom, bottomInset);
+ bottomScrollIndicatorInset = nfloat.Max(StartingScrollIndicatorInsets.Bottom, bottomScrollIndicatorInset);
+
+ if (OperatingSystem.IsIOSVersionAtLeast(11, 0))
+ {
+ bottomInset -= scrolledView.SafeAreaInsets.Bottom;
+ bottomScrollIndicatorInset -= scrolledView.SafeAreaInsets.Bottom;
+ }
+
+ var movedInsets = scrolledView.ContentInset;
+ movedInsets.Bottom = bottomInset;
+
+ // if we are in an editor that is inside a scrollView and are below where the keyboard will appear,
+ // the outer scrollview will put the cursor above the keyboard and we will
+ // need to add a bottom inset to the inner editor so that the cursor will
+ // stay above the keyboard when we add new lines.
+ if (didMove && isInnerEditor && scrolledView is UITextView textView)
+ {
+ var cursorRect = FindCursorPosition();
+ if (cursorRect is CGRect cursor)
+ {
+ var editorBottomInset = frameInWindow.Bottom - cursor.Bottom - TextViewDistanceFromBottom;
+ movedInsets.Bottom = nfloat.Max(0, editorBottomInset);
+ bottomScrollIndicatorInset = nfloat.Max(0, editorBottomInset);
+ }
+ }
+
+ if (lastScrollView.ContentInset != movedInsets)
+ {
+ UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateInset(scrolledView, movedInsets, bottomScrollIndicatorInset), () => { });
+ }
}
static UIScrollView? FindParentScroll(UIScrollView? view)
{
while (view is not null)
{
- if (view.ScrollEnabled)
+ if (view.ScrollEnabled && !IsHorizontalCollectionView(view))
+ {
return view;
+ }
view = view.FindResponder();
}
@@ -685,10 +839,15 @@ static void AnimateRootView(CGRect rect)
return null;
}
+ static bool IsHorizontalCollectionView(UIView collectionView)
+ => collectionView is UICollectionView { CollectionViewLayout: UICollectionViewFlowLayout { ScrollDirection: UICollectionViewScrollDirection.Horizontal }};
+
internal static nfloat FindKeyboardHeight()
{
if (ContainerView is null)
+ {
return 0;
+ }
var window = ContainerView.Window;
var intersectRect = CGRect.Intersect(KeyboardFrame, window.Frame);
@@ -706,10 +865,11 @@ static nfloat AdjustForLargeTitles(nfloat move, UIScrollView superScrollView, UI
// so skip if we are not in those scenarios.
if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone
&& (UIDevice.CurrentDevice.Orientation == UIDeviceOrientation.LandscapeLeft || UIDevice.CurrentDevice.Orientation == UIDeviceOrientation.LandscapeRight))
+ {
return move;
+ }
- // These values are not publicly available but can be tested.
- // It is possible that these can change in the future.
+ // These values are not publicly available but can be tested. It is possible that these can change in the future.
var navBarCollapsedHeight = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone ? 44 : 50;
var navBarExpandedHeight = navController.NavigationBar.SizeThatFits(new CGSize(0, 0)).Height;
@@ -726,12 +886,15 @@ static nfloat AdjustForLargeTitles(nfloat move, UIScrollView superScrollView, UI
// to the minimum amount that will cause the collapse or else
// we will not see our view
if (move - navBarCollapseDifference < amountLeftToCollapseNavBar)
+ {
return amountLeftToCollapseNavBar;
+ }
- // else the navBar will collapse and we want to subtract
- // the navBarCollapseDifference to account for it
+ // else the navBar will collapse and we want to subtract the navBarCollapseDifference to account for it
else
+ {
return move - navBarCollapseDifference;
+ }
}
return move;
}
@@ -750,14 +913,20 @@ static void RestorePosition()
}
if (ScrolledView is not null && ScrolledView.ContentInset != UIEdgeInsets.Zero)
+ {
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateInset(ScrolledView, UIEdgeInsets.Zero, 0), () => { });
+ }
+
+ if (View is not null && View is UIScrollView editorScrollView && editorScrollView.ContentInset != UIEdgeInsets.Zero && View is UITextView textView)
+ {
+ UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateInset(editorScrollView, UIEdgeInsets.Zero, 0), () => { });
+ }
ScrolledView = null;
View = null;
ContainerView = null;
TopViewBeginOrigin = InvalidPoint;
CursorRect = null;
- StartingContainerViewFrame = null;
ShouldIgnoreSafeAreaAdjustment = false;
ShouldScrollAgain = false;
}
@@ -771,16 +940,26 @@ static void RestorePosition()
{
previousSection -= 1;
if (previousSection >= 0 && scrollView is UICollectionView collectionView)
+ {
previousRow = (int)(collectionView.NumberOfItemsInSection(previousSection) - 1);
+ }
else if (previousSection >= 0 && scrollView is UITableView tableView)
+ {
previousRow = (int)(tableView.NumberOfRowsInSection(previousSection) - 1);
+ }
else
+ {
return null;
+ }
}
if (previousRow >= 0 && previousSection >= 0)
+ {
return NSIndexPath.FromRowSection(previousRow, previousSection);
+ }
else
+ {
return null;
+ }
}
}
diff --git a/src/Core/src/Platform/iOS/MauiTextView.cs b/src/Core/src/Platform/iOS/MauiTextView.cs
index 8b8fd9f520f4..b4ddaaf77f7b 100644
--- a/src/Core/src/Platform/iOS/MauiTextView.cs
+++ b/src/Core/src/Platform/iOS/MauiTextView.cs
@@ -171,7 +171,7 @@ void ShouldCenterVertically()
{
Maui.TextAlignment.Center => new CGPoint(0, -Math.Max(1, availableSpace / 2)),
Maui.TextAlignment.End => new CGPoint(0, -Math.Max(1, availableSpace)),
- _ => new CGPoint(0, 0),
+ _ => ContentOffset,
};
}
diff --git a/src/Core/src/Platform/iOS/MauiView.cs b/src/Core/src/Platform/iOS/MauiView.cs
index ebc05dd871f0..c2d04b68e4e6 100644
--- a/src/Core/src/Platform/iOS/MauiView.cs
+++ b/src/Core/src/Platform/iOS/MauiView.cs
@@ -34,7 +34,9 @@ bool RespondsToSafeArea()
protected CGRect AdjustForSafeArea(CGRect bounds)
{
if (KeyboardAutoManagerScroll.ShouldIgnoreSafeAreaAdjustment)
+ {
KeyboardAutoManagerScroll.ShouldScrollAgain = true;
+ }
if (View is not ISafeAreaView sav || sav.IgnoreSafeArea || !RespondsToSafeArea())
{