MonoTouch: Auto scroll UITextField or other views hidden by keyboard

Here is a snippet that I use in my base view controller class to handle scrolling views that would be hidden by the keyboard when it appears. Any view within a UIScrollView can be made to scroll automatically to be visible when the keyboard appears over it.

This is based on the following Apple documentation example: Managing the Keyboard: Moving Content That Is Located Under the Keyboard.

This differs from the Apple example in that it uses the UIKeyboardWillShowNotification instead of UIKeyboardDidShowNotification, and also animates resetting the scrolling when hiding the keyboard. The code can also handle content that might already be scrolled and I have found that the positioning of the UIView’s is more accurate in complicated view hierarchies.

I place this in my base view controller “BaseViewController” which I descend all my other custom UIViewController’s from. You could put this in each view controller that needs it directly if you wanted.

If I want to support auto keyboard scrolling in a particular view controller, I simply put a UIScrollView at the top of the view hierarchy and any view that is the first responder within this UIScrollView when the UIKeyboard is shown will be scrolled into view.

When I need to control what is scrolled into view I override the GetKeyboardActiveView. E.g. for a login view where I want the login button to also be visible I will return the UIButton or perhaps a view that contains/defines the bounds of all the login text fields and button. Then when the password field is active, the login button is also visible.

public override void ViewDidLoad ()
{
	base.ViewDidLoad ();

	// Setup keyboard event handlers
	RegisterForKeyboardNotifications();
}

public override void ViewDidUnload()
{
	base.ViewDidUnload();
			
	UnregisterKeyboardNotifications();
}
		
NSObject _keyboardObserverWillShow;
NSObject _keyboardObserverWillHide;
		
protected virtual void RegisterForKeyboardNotifications ()
{
	_keyboardObserverWillShow = NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillShowNotification, KeyboardWillShowNotification);
	_keyboardObserverWillHide = NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillHideNotification, KeyboardWillHideNotification);
}
		
protected virtual void UnregisterKeyboardNotifications()
{
	NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardObserverWillShow);
	NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardObserverWillHide);
}

/// <summary>
/// Gets the UIView that represents the "active" user input control (e.g. textfield, or button under a text field)
/// </summary>
/// <returns>
/// A <see cref="UIView"/>
/// </returns>
protected virtual UIView KeyboardGetActiveView()
{
	return this.View.FindFirstResponder();
}

protected virtual void KeyboardWillShowNotification (NSNotification notification)
{
	UIView activeView = KeyboardGetActiveView();
	if (activeView == null)
		return;

	UIScrollView scrollView = activeView.FindSuperviewOfType(this.View, typeof(UIScrollView)) as UIScrollView;
	if (scrollView == null)
		return;

	RectangleF keyboardBounds = UIKeyboard.BoundsFromNotification(notification);

	UIEdgeInsets contentInsets = new UIEdgeInsets(0.0f, 0.0f, keyboardBounds.Size.Height, 0.0f);
	scrollView.ContentInset = contentInsets;
	scrollView.ScrollIndicatorInsets = contentInsets;

	// If activeField is hidden by keyboard, scroll it so it's visible
	RectangleF viewRectAboveKeyboard = new RectangleF(this.View.Frame.Location, new SizeF(this.View.Frame.Width, this.View.Frame.Size.Height - keyboardBounds.Size.Height));

	RectangleF activeFieldAbsoluteFrame = activeView.Superview.ConvertRectToView(activeView.Frame, this.View);
	// activeFieldAbsoluteFrame is relative to this.View so does not include any scrollView.ContentOffset

	// Check if the activeField will be partially or entirely covered by the keyboard
	if (!viewRectAboveKeyboard.Contains(activeFieldAbsoluteFrame))
	{
		// Scroll to the activeField Y position + activeField.Height + current scrollView.ContentOffset.Y - the keyboard Height
		PointF scrollPoint = new PointF(0.0f, activeFieldAbsoluteFrame.Location.Y + activeFieldAbsoluteFrame.Height + scrollView.ContentOffset.Y - viewRectAboveKeyboard.Height);
		scrollView.SetContentOffset(scrollPoint, true);
	}
}

protected virtual void KeyboardWillHideNotification (NSNotification notification)
{
	UIView activeView = KeyboardGetActiveView();
	if (activeView == null)
		return;

	UIScrollView scrollView = activeView.FindSuperviewOfType (this.View, typeof(UIScrollView)) as UIScrollView;
	if (scrollView == null)
		return;

	// Reset the content inset of the scrollView and animate using the current keyboard animation duration
	double animationDuration = UIKeyboard.AnimationDurationFromNotification(notification);
	UIEdgeInsets contentInsets = new UIEdgeInsets(0.0f, 0.0f, 0.0f, 0.0f);
	UIView.Animate(animationDuration, delegate{
		scrollView.ContentInset = contentInsets;
		scrollView.ScrollIndicatorInsets = contentInsets;
	});
}

Here are the UIView extensions for FindFirstResponder and FindSuperviewOfType:

public static class ViewExtensions
{
	/// <summary>
	/// Find the first responder in the <paramref name="view"/>'s subview hierarchy
	/// </summary>
	/// <param name="view">
	/// A <see cref="UIView"/>
	/// </param>
	/// <returns>
	/// A <see cref="UIView"/> that is the first responder or null if there is no first responder
	/// </returns>
	public static UIView FindFirstResponder (this UIView view)
	{
		if (view.IsFirstResponder)
		{
			return view;
		}
		foreach (UIView subView in view.Subviews) {
			var firstResponder = subView.FindFirstResponder();
			if (firstResponder != null)
				return firstResponder;
		}
		return null;
	}

	/// <summary>
	/// Find the first Superview of the specified type (or descendant of)
	/// </summary>
	/// <param name="view">
	/// A <see cref="UIView"/>
	/// </param>
	/// <param name="stopAt">
	/// A <see cref="UIView"/> that indicates where to stop looking up the superview hierarchy
	/// </param>
	/// <param name="type">
	/// A <see cref="Type"/> to look for, this should be a UIView or descendant type
	/// </param>
	/// <returns>
	/// A <see cref="UIView"/> if it is found, otherwise null
	/// </returns>
	public static UIView FindSuperviewOfType(this UIView view, UIView stopAt, Type type)
	{
		if (view.Superview != null)
		{
			if (type.IsAssignableFrom(view.Superview.GetType()))
			{
				return view.Superview;
			}

			if (view.Superview != stopAt)
				return view.Superview.FindSuperviewOfType(stopAt, type);
		}

		return null;
	}
}

16 thoughts on “MonoTouch: Auto scroll UITextField or other views hidden by keyboard”

  1. Don’t know how you haven’t got any comments saying this already, but this is excellent – thank you!

  2. Outstanding! I had a version I was pretty proud of written in Objective-C — very similar to this conceptually. On a deadline and was not looking forward to porting mine. Yours works perfectly for me.

    In the KeyboardWillShowNotification method, the following line needs to be replaced (obsolete API):

    RectangleF keyboardBounds = UIKeyboard.BoundsFromNotification(notification);

    with this one:

    RectangleF keyboardBounds = UIKeyboard.FrameBeginFromNotification(notification);

    Thanks!

  3. I also fix one UI Bug.
    When Textbox is partially overlay by keyboard, it does not scroll.

    Change:
    // Check if the activeField will be partially or entirely covered by the keyboard
    if (!viewRectAboveKeyboard.Contains(activeFieldAbsoluteFrame))
    {
    // Scroll to the activeField Y position + activeField.Height + current scrollView.ContentOffset.Y – the keyboard Height
    PointF scrollPoint = new PointF(0.0f, activeFieldAbsoluteFrame.Location.Y + activeFieldAbsoluteFrame.Height + scrollView.ContentOffset.Y – viewRectAboveKeyboard.Height);
    scrollView.SetContentOffset(scrollPoint, true);
    }

    To:
    // Scroll to the activeField Y position + activeField.Height + current scrollView.ContentOffset.Y – the keyboard Height
    PointF scrollPoint = new PointF (0.0f, activeFieldAbsoluteFrame.Location.Y + activeFieldAbsoluteFrame.Height + scrollView.ContentOffset.Y – viewRectAboveKeyboard.Height);
    // Check if the activeField will be partially or entirely covered by the keyboard
    if (viewRectAboveKeyboard.IntersectsWith (activeFieldAbsoluteFrame) &&
    ! viewRectAboveKeyboard.Contains (activeFieldAbsoluteFrame)) {
    scrollView.SetContentOffset (scrollPoint, true);
    } else if (!viewRectAboveKeyboard.Contains (activeFieldAbsoluteFrame)) {
    scrollView.SetContentOffset (scrollPoint, true);
    }

  4. Ok, I’m a beginner here. What do I do with the UIView Extensions to make them get used by my UIViewController?

  5. With iOS 6 and iPad version I had problem, so here is my code, which works great.

    First, replace the ViewDidUnload by ViewDidDisappear (bool animated) and and ViewDidLoad by ViewWillAppear (bool animated)

    (base constructor will take animated args).

    Then, replace all the KeyboardWillShowNotification (NSNotification notification)
    by

    protected virtual void KeyboardWillShowNotification (NSNotification notification)
    {
    UIView activeView = KeyboardGetActiveView();
    if (activeView == null)
    return;

    UIScrollView scrollView = activeView.FindSuperviewOfType(this.View, typeof(UIScrollView)) as UIScrollView;
    if (scrollView == null)
    return;

    RectangleF keyboardBounds = UIKeyboard.FrameBeginFromNotification(notification);

    UIEdgeInsets contentInsets = new UIEdgeInsets(0.0f, 0.0f, keyboardBounds.Size.Height, 0.0f);
    scrollView.ContentInset = contentInsets;
    scrollView.ScrollIndicatorInsets = contentInsets;

    // If activeField is hidden by keyboard, scroll it so it’s visible
    RectangleF viewRectAboveKeyboard = new RectangleF(this.View.Frame.Location, new SizeF(this.View.Frame.Width, this.View.Frame.Size.Height – keyboardBounds.Size.Height * 0.5f));

    RectangleF activeFieldAbsoluteFrame = activeView.Superview.ConvertRectToView(activeView.Frame, this.View);
    // activeFieldAbsoluteFrame is relative to this.View so does not include any scrollView.ContentOffset

    // Check if the activeField will be partially or entirely covered by the keyboard
    if (!viewRectAboveKeyboard.Contains(activeFieldAbsoluteFrame))
    {
    // Scroll to the activeField Y position + activeField.Height + current scrollView.ContentOffset.Y – the keyboard Height
    PointF scrollPoint = new PointF(0.0f, activeFieldAbsoluteFrame.Location.Y – viewRectAboveKeyboard.Height * 0.5f);
    scrollView.SetContentOffset(scrollPoint, true);
    }
    }

  6. I am a beginner. I created a baseviewcontroller and copied all the above code to it. I have inherited it to my signupviewcontroller like this

    public partial class signupviewcontroller:baseviewcontroller

    and the doubt is about the constructor here.
    In signupviewcontroller the constructor is like this.

    public signupviewcontroller () : base (“signupviewcontroller”,null)
    {
    }

    and in the baseviewcontroller it is like this

    public baseviewcontroller () : base (“baseviewcontroller”,null)
    {
    }

    In this way it shows error- there is no 2 arguments for the constructor in baseviewcontroller. What is the right way to write the constructor? please reply me soon

    1. It’s been a while since I’ve looked at this code, but try creating another constructor override in the baseviewcontroller that accepts the string etc also.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.