Search code examples
iosxamarinkeyboarduitextfield

Stop Keyboard From Hiding UITextField When Open


Looking for a solution that stops a UITextField from being hidden by the keyboard when it opens regardless of where the UITextField is in the view hierarchy and regardless of other UI elements.


Solution

  • There are plenty of solutions online for adjusting your view so that a UITextField isn't hidden behind the keyboard when it appears however, I couldn't find a "one size fits all" kind of solution so had to adapt a lot of answers and make my own. This is the solution I came up with. Hopefully it helps someone :)

    First of all, this solution assumes that your "primary parent" or "top level" view is a UIScrollView or descendant (e.g. UITableView or UICollectionView). This will not work if you don't have a UIScrollView in your hierarchy as this is what is used to scroll the UITextField into position. Other solutions show you how to scroll any UIView but when you think about it, if you need to scroll your UITextField into position, chances are your view should be scrollable anyway to stop it from going off the screen.

    Create the extension method below. This searches for subviews of subviews (etc.) that match the given query and returns them as an array. We'll use this later.

    public static UIView[] Find(this UIView view, Func<UIView, bool> query)
    {
        if (view == null)
            return null;
    
        var views = new List<UIView>();
    
        if (query.Invoke(view))
            views.Add(view);
    
        foreach (var subview in view.Subviews)
        {
            var foundViews = subview.Find(query);
    
            if (foundViews != null)
                views.AddRange(foundViews);
        }
    
        return views.ToArray();
    }
    

    Add the below method to your UIViewController. This finds the top level UIScrollView from the view hierarchy. There should only be one result.

    private UIScrollView FindAdjustmentScrollView()
    {
        var scrollViews = View.Find(v => v is UIScrollView && v.FindSuperviewOfType(View, typeof(UIScrollView)) == null);
    
        return scrollViews.Length > 0 ? scrollViews[0] as UIScrollView : null;
    }
    

    Add the below event handlers. We'll register these with observers next.

    private void Keyboard_Appear(NSNotification notification)
    {
        var firstResponder = View.Find(v => v.IsFirstResponder).FirstOrDefault();
    
        var scrollView = FindAdjustmentScrollView();
    
        if (firstResponder == null || scrollView == null || !(notification.UserInfo[UIKeyboard.FrameEndUserInfoKey] is NSValue value))
            return;
    
        var keyboardBounds = value.CGRectValue;
        var firstResponderAbsoluteFrame = firstResponder.Superview.ConvertRectToView(firstResponder.Frame, View);
    
        // This is how much of a gap you would like there to be between the bottom of the UITextField
        // and the top of the keyboard. Not mandatory but a nice touch in my experience.
        var offset = 8;
    
        var bottom = firstResponderAbsoluteFrame.Y + firstResponderAbsoluteFrame.Height + offset;
        var scrollAmount = keyboardBounds.Height - (scrollView.Frame.Size.Height - bottom);
    
        if (scrollAmount > 0)
            scrollView.SetContentOffset(0, scrollView.ContentOffset.Y + scrollAmount);
    }
    
    private void Keyboard_Disappear(NSNotification notification)
    {
        var firstResponder = View.Find(v => v.IsFirstResponder).FirstOrDefault();
    
        var scrollView = FindAdjustmentScrollView();
    
        if (firstResponder == null || scrollView == null)
            return;
    
        scrollView.SetContentOffset(0, 0);
    }
    

    Add these 2 fields to your UIViewController. By keeping an object reference to the observers we can unregister them later when the UIViewController is no longer visible.

    private NSObject _keyboardWillShowObserver, _keyboardWillHideObserver;
    

    In ViewWillAppear (or wherever you register your event handlers) add these 2 lines. These register the observers and keep an object reference so we can unregister later.

    _keyboardWillShowObserver = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, Keyboard_Appear);
    _keyboardWillHideObserver = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, Keyboard_Disappear);
    

    In ViewWillDisappear (or wherever you unregister your event handlers) add these 2 lines. These unregister the observers from the UIViewController so they no longer respond to events.

    NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardWillShowObserver);
    NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardWillHideObserver);
    

    To clarify a few things:

    • This solution works when your UITextField is a subview many layers down (even a UITableViewCell or UICollectionViewCell).
    • This solution does take into account any modifications made to the keyboard view. For example: if you add a "done" button to the top of your keyboard this will be calculated.
    • The scroll adjustment is animated.
    • This solution does work in different orientations.