Search code examples
c#iosxamarin.ios

Xamarin.iOS - Need to move entire view up on text entry


I have a view that contains a UIScrollview with various UITextFields contained within the scrollview. When I tap on an UITextField to enter data, the UITextField scrolls up unter the subviews above the UIScrolView and is hidden from view. I'm using a kewboard notification solution I found here on StackOverflow, but it doesn't work when a scrollview is present. What do I need to do to force the entire view up so that the text entry is visible?

My Code:

    public override void ViewDidLoad()
    {
        this.NavigationItem.SetHidesBackButton(true, false);
        btnExit.Hidden = false;
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
        var NavBarHeight = UIApplication.SharedApplication.StatusBarFrame.Size.Height +
                           (NavigationController?.NavigationBar.Frame.Height ?? 0.0);
        RegisterForKeyboardNotifications();

        scrollView = new UIScrollView
        {
            Frame = new CGRect(0, 0, View.Frame.Width, h + 2 * padding),
            ContentSize = new SizeF((w + padding) * 7, (h + padding) * n),
            BackgroundColor = UIColor.White,
            AutoresizingMask = UIViewAutoresizing.FlexibleWidth
        };
        .
        . 
        .
        .        Bunch of irrelevant code creating subviews to add to scrollview       
        .             
        .             
        . 

        scrollView.Add(lblDOB);
        scrollView.Add(txtDOB);
        scrollView.Add(lblFirstName);
        scrollView.Add(txtFirstName);
        scrollView.Add(lblMI);
        scrollView.Add(txtMI);
        scrollView.Add(lblLastName);
        scrollView.Add(txtLastName);
        scrollView.Add(lblSuffix);
        scrollView.Add(txtSuffix);
        scrollView.Add(lblAddress);
        scrollView.Add(txtAddress);
        scrollView.Add(lblCity);
        scrollView.Add(txtCity);
        scrollView.Add(lblState);
        scrollView.Add(txtState);
        scrollView.Add(lblZip);
        scrollView.Add(txtZip);
        View.Add(scrollView);

        scrollView.BackgroundColor = UIKit.UIColor.White;
        scrollView.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();

        scrollView.AddConstraints(
            lblDOB.AtTopOf(scrollView, 0),
            lblDOB.AtLeftOf(scrollView, 0),
            txtDOB.ToRightOf(lblDOB, 2),
            txtDOB.Width().EqualTo(250.0f),
            lblFirstName.Below(lblDOB, 5),
            txtFirstName.Below(lblDOB, 0).Plus(5),
            txtFirstName.ToRightOf(lblFirstName, 2),
            txtFirstName.Width().EqualTo(250.0f),
            lblMI.Below(lblFirstName, 5),
            txtMI.Below(lblFirstName, 0).Plus(5),
            txtMI.ToRightOf(lblMI, 2),
            txtMI.Width().EqualTo(50.0f),
            lblLastName.Below(lblMI, 5),
            txtLastName.Below(lblMI, 0).Plus(5),
            txtLastName.ToRightOf(lblLastName, 2),
            txtLastName.Width().EqualTo(250.0f),
            lblSuffix.Below(lblLastName, 5),
            txtSuffix.Below(lblLastName, 0).Plus(5),
            txtSuffix.ToRightOf(lblSuffix, 2),
            txtSuffix.Width().EqualTo(50.0f),
            lblAddress.Below(lblSuffix, 5),
            txtAddress.Below(lblSuffix, 0).Plus(5),
            txtAddress.ToRightOf(lblAddress, 2),
            txtAddress.Width().EqualTo(250.0f),
            lblCity.Below(lblAddress, 5),
            txtCity.Below(lblAddress, 0).Plus(5),
            txtCity.ToRightOf(lblCity, 2),
            txtCity.Width().EqualTo(250.0f),
            lblState.Below(lblCity, 5),
            txtState.Below(lblCity, 0).Plus(5),
            txtState.ToRightOf(lblState, 2),
            txtState.Width().EqualTo(250.0f),
            lblZip.Below(lblState, 5),
            txtZip.Below(lblState, 0).Plus(5), 
            txtZip.ToRightOf(lblZip, 2),
            txtZip.Width().EqualTo(250.0f)
        );

        View.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
        View.AddConstraints
        (
            banner.AtTopOf(View, (float)NavBarHeight),
            banner.AtRightOf(View, 0),
            banner.AtLeftOf(View, 0),
            employeeimage.Below(banner, 2),
            employeeimage.Height().EqualTo(180),
            employeeimage.Width().EqualTo(180),
            btnSave.AtTopOf(employeeimage),
            btnSave.ToRightOf(employeeimage, 5),
            btnExit.Below(btnSave),
            btnExit.WithSameLeft(btnSave),
            btnExit.WithSameWidth(btnSave),
            lblName.Below(employeeimage, 2),
            lblName.WithSameWidth(banner),
            line.Below(lblName, 0),
            line.WithSameWidth(banner),
            line.Height().EqualTo(2),
            scrollView.Below(line, 0),
            scrollView.AtBottomOf(View, 10),
            scrollView.AtLeftOf(View, 10),
            scrollView.AtRightOf(View, 10)
        );

The view displays correctly with all subviews being present. The code that is supposed to push the view up when the keyboard pops up is:

    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);
    }

    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 = (RectangleF)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((PointF)this.View.Frame.Location, new SizeF((float)this.View.Frame.Width, (float)(this.View.Frame.Size.Height - keyboardBounds.Size.Height)));

        RectangleF activeFieldAbsoluteFrame = (RectangleF)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, (float)(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;
        });
    }

View extensions class:

public static class ViewExtensions
{
    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;
    }

   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;
    }

    
}

When the DOB textview is tapped, the textview disappears under the subviews located above the scrollview, I've tried everything I can think of and various methods I've found online, but nothing seems to have any effect. Any suggestions?


Solution

  • Well, I was unable to make the code in the question work like I needed it to, but I did find this:

    ViewController with scroll when appear keyboard

    It's almost perfect, I installed the IQKeyboardManager from NuGet and added the following code to my app in the initial viewcontroller:

        Xamarin.IQKeyboardManager.SharedManager.EnableAutoToolbar = true;
        Xamarin.IQKeyboardManager.SharedManager.ShouldResignOnTouchOutside = true;
        Xamarin.IQKeyboardManager.SharedManager.ShouldToolbarUsesTextFieldTintColor = true;
        Xamarin.IQKeyboardManager.SharedManager.KeyboardDistanceFromTextField = 150f;
    

    I just need to figure out how to stop the scrollview from pushing up, but I can live with the default as the user can scroll the text field back into view, but I think I can get it to stop doing that. Otherwise, this works VERY well.

    **** Update ****

    I was able to stop the scrollview from scrolling the UITextField up out of sight by using the UITextField delegate. I added the following code to my view:

    First I added IUITextFieldDelegate to the view controller:

    public partial class EmployeeInfo : UIViewController, IUITextFieldDelegate
    

    Next I added the following code for each UITextField:

    MyUITextField.Delegate = this;
    

    I did this for each and every UITextField added to to scrollview.

    Then added the following two functions in the ViewDidLoad():

        [Export("textFieldDidBeginEditing:")]
        public void EditingStarted(UITextField textfield)
        {
            this.scrollView.ScrollEnabled = false;// freeze the scrollview
            CGPoint point = textfield.Frame.Location;
            point.X = 0; // make sure they can see the identifying label
            this.scrollView.SetContentOffset(point, true);
        }
    
        [Export("textFieldDidEndEditing:reason:")]
        public void EditingEnded(UITextField textField, UITextFieldDidEndEditingReason reason)
        {
            this.scrollView.ScrollEnabled = true; // Re-enable the scrollview
        }
    

    If there's a drawback, it's that one cannot scroll the scrollview while editing a textview, but that's a minor issue. If you tap into another textfield, the scrollview position is updated.