Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Fix Entry Next Keyboard Button Finds Next TextField #11914

Merged
merged 14 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions THIRD-PARTY-NOTICES.TXT
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,31 @@ License notice for Gradle (https://github.com/gradle/gradle)

==============================================================================


License notice for IQKeyboardManager
=========================================

(https://github.com/hackiftekhar/IQKeyboardManager/blob/master/LICENSE.md)

MIT License

Copyright (c) 2013-2017 Iftekhar Qurashi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
=========================================
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ static bool OnShouldReturn(UITextField view)
if (handler != null)
handler(realCell, EventArgs.Empty);

view.ResignFirstResponder();
KeyboardAutoManager.GoToNextResponderOrResign(view);
return true;
}

Expand Down
5 changes: 2 additions & 3 deletions src/Core/src/Handlers/Entry/EntryHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.Maui.Graphics;
using ObjCRuntime;
using UIKit;
using Microsoft.Maui.Platform;

namespace Microsoft.Maui.Handlers
{
Expand Down Expand Up @@ -122,9 +123,7 @@ public static void MapFormatting(IEntryHandler handler, IEntry entry)

protected virtual bool OnShouldReturn(UITextField view)
{
view.ResignFirstResponder();

// TODO: Focus next View
KeyboardAutoManager.GoToNextResponderOrResign(view);
tj-devel709 marked this conversation as resolved.
Show resolved Hide resolved

VirtualView?.Completed();

Expand Down
50 changes: 50 additions & 0 deletions src/Core/src/Platform/iOS/KeyboardAutoManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* This class is adapted from IQKeyboardManager which is an open-source
* library implemented for iOS to handle Keyboard interactions with
* UITextFields/UITextViews. Link to their MIT License can be found here:
* https://github.com/hackiftekhar/IQKeyboardManager/blob/7399efb730eea084571b45a1a9b36a3a3c54c44f/LICENSE.md
*/

using System;
using UIKit;

namespace Microsoft.Maui.Platform;

internal static class KeyboardAutoManager
{
internal static void GoToNextResponderOrResign(UIView view, UIView? customSuperView = null)
{
if (!view.CheckIfEligible())
{
view.ResignFirstResponder();
return;
}

var superview = customSuperView ?? view.FindResponder<ContainerViewController>()?.View;
if (superview is null)
{
view.ResignFirstResponder();
return;
}

var nextField = view.FindNextView(superview, view =>
{
var isValidTextView = view is UITextView textView && textView.Editable;
var isValidTextField = view is UITextField textField && textField.Enabled;

return (isValidTextView || isValidTextField) && !view.Hidden && view.Alpha != 0f;
});

view.ChangeFocusedView(nextField);
}

static bool CheckIfEligible(this UIView view)
{
if (view is UITextField field && field.ReturnKeyType == UIReturnKeyType.Next)
return true;
else if (view is UITextView)
return true;

return false;
}
}
61 changes: 61 additions & 0 deletions src/Core/src/Platform/iOS/ViewExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -698,5 +698,66 @@ internal static void UpdateLayerBorder(this CoreAnimation.CALayer layer, IButton
if (stroke.CornerRadius >= 0)
layer.CornerRadius = stroke.CornerRadius;
}

internal static T? FindResponder<T>(this UIView view) where T : UIResponder
{
var nextResponder = view as UIResponder;
while (nextResponder is not null)
{
nextResponder = nextResponder.NextResponder;

if (nextResponder is T responder)
return responder;
}
return null;
}

internal static UIView? FindNextView(this UIView view, UIView superView, Func<UIView, bool> isValidType)
{
var passedOriginal = false;

var nextView = superView.FindNextView(view, ref passedOriginal, isValidType);
Copy link
Member

Choose a reason for hiding this comment

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

Instead of always starting at the top level view and searching down to find your place can we start the search from where we are at and search out? This way we aren't always traversing the entire view stack?


// if we did not find the next view, try to find the first one
nextView ??= superView.FindNextView(null, ref passedOriginal, isValidType);

return nextView;
}

static UIView? FindNextView(this UIView view, UIView? origView, ref bool passedOriginal, Func<UIView, bool> isValidType)
{
foreach (var child in view.Subviews)
{
if (isValidType(child))
{
if (origView is null)
return child;

if (passedOriginal)
return child;

if (child == origView)
passedOriginal = true;
}

else if (child.Subviews.Length > 0 && !child.Hidden && child.Alpha > 0f)
{
var nextLevel = child.FindNextView(origView, ref passedOriginal, isValidType);
if (nextLevel is not null)
return nextLevel;
}
}

return null;
}

internal static void ChangeFocusedView(this UIView view, UIView? newView)
{
if (newView is null)
view.ResignFirstResponder();

else
newView.BecomeFirstResponder();
}
}
}
Loading