10
votes

I have a UITextField inside a UIScrollView (a few levels deep). I am watching UIKeyboardDidShowNotification, and also calling the same code when I manually change the first responder (I might change to a different text field without momentarily hiding the keyboard). In that code I use scrollRectToVisible:animated: to make sure the UITextField is visible.

I was having a huge headache debugging why that was acting funny, but I realized now that UIScrollView automatically ensures that the first responder is within its bounds. I am changing the frame of the UIScrollView so that none of it is hidden behind the keyboard.

However, my code can be slightly smarter than their code, because I want to show not only the UITextField, but some nearby related views as well. I try to show those views if they will fit; if not whatever, I try to show as much of them as I can but at least ensure that the UITextField is visible. So I want to keep my custom code.

The automatic behavior interferes with my code. What I see is the scroll view gently scroll up so that the bottom edge of my content is visible, then it snaps down to where my code told it to position.

Is there anyway to stop the UIScrollView from doing its default capability of scrolling the first responder into view?

More Info

On reviewing the documentation I read that they advise to change the scroll view's contentInset instead of frame. I changed that and eliminated some unpredictable behavior, but it didn't fix this particular problem.

I don't think posting all the code would necessarily be that useful. But here is the critical call and the values of important properties at that time. I will just write 4-tuples for CGRects; I mean (x, y, width, height).

[scrollView scrollRectToVisible:(116.2, 71.2, 60, 243) animated:YES];

scrollView.bounds == (0, 12, 320, 361)

scrollView.contentInset == UIEdgeInsetsMake(0, 0, 118, 0)

textField.frame == (112.2, 222.6, 24, 24)

converted to coordinates of the immediate subview of scrollView == (134.2, 244.6, 24, 24)

converted to coordinates of scrollView == (134.2, 244.6, 24, 24)

So the scroll view bottom edge is really at y == 243 because of the inset.

The requested rectangle extends to y == 314.2.

The text field extends to y == 268.6.

Both are out of bounds. scrollRectToVisible is trying to fix one of those problems. The standard UIScrollView / UITextField behavior is trying to fix the other. They don't come up with quite the same solution.

5
For what it's worth, I'm letting the default behavior apply and it's not bothering me at all. The results of the automatic scrolling are not that much different from what my code would achieve.morningstar
Not sure if it matters at this point, but I commented on a solution below that works pretty well. A zero second delay before calling your own scrollRectToVisible:animated: in the keyboard show handler seems to "override" the behavior.Michael McGuire

5 Answers

4
votes

I didn't test this particular situation, but I've managed to prevent a scrollview from bouncing at the top and bottom by subclassing the scrollview and overriding setContentOffset: and setContentOffset:animated:. The scrollview calls this at every scroll movement, so I'm fairly certain they will be called when scrolling to the textfield.

You can use the delegate method textFieldDidBeginEditing: to determine when the scroll is allowed.

In code:

- (void)textFieldDidBeginEditing:(UITextField *)textField
{
    self.blockingTextViewScroll = YES;
}

-(void)setContentOffset:(CGPoint)contentOffset
{
    if(self.blockingTextViewScroll)
    {
        self.blockingTextViewScroll = NO;
    }
    else
    {
        [super setContentOffset:contentOffset];
    }
}


-(void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
    if(self.blockingTextViewScroll)
    {
        self.blockingTextViewScroll = NO;
    }
    else
    {
        [super setContentOffset:contentOffset animated:animated];
    }
}

If your current scroll behaviour works with a setContentOffset: override, just place it inside the else blocks (or preferably, in a method you call from the else blocks).

2
votes

In my project I have succeeded to achieve this by performing my scroll only after some delay.

- (void)keyboardWillShow:(NSNotification *)note
{
    NSDictionary *userInfo = note.userInfo;
    CGRect keyboardFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];

    UIEdgeInsets contentInsets = self.tableView.contentInset;
    contentInsets.bottom += keyboardFrame.size.height;

    [self.tableView setContentInset:contentInsets];
     [self performSelector:@selector(scrollToEditableCell) withObject:nil afterDelay:0];
}

Also there is other possibility to make your view with additional views to be first responder and fool scroll view where to scroll. Haven't tested this yet.

0
votes

This may turn out to be useless, but have you tried setting scrollView.userInteractionEnabled to NO before calling scrollrectToVisible: & then setting it back to YES? It may prevent the automatic scrolling behavior.

0
votes

Try changing the view autoresizing to UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin. The default is FlexibleTopMargin so maybe thats the reason. btw scrollRectToVisible: is using the scrollView.contentSize.

The other thing you can try to change the scrollView size first and then apply the scrollRectToVisible: change. First frame change, then content change. (Maybe observe the keyboard did appear event)

0
votes

The automatic scrolling behavior seems to be especially buggy starting in iOS 14. I alleviated the problem by subclassing UIScrollView and overriding setContentOffset to do nothing. Here is the bases of my code.

class ManualScrollView: UIScrollView {

    /// Use this function to set the content offset. This will forward the call to
    /// super.setContentOffset(:animated:)
    /// - Parameters:
    ///   - contentOffset: A point (expressed in points) that is offset from the content view’s origin.
    ///   - animated: true to animate the transition at a constant velocity to the new offset, false to make the transition immediate.
    func forceContentOffset(_ contentOffset: CGPoint, animated: Bool) {
        super.setContentOffset(contentOffset, animated: animated)
    }

    /// This function has be overriden to do nothing to block system calls from changing the
    /// content offset at undesireable times.
    ///
    /// Instead call forceContentOffset(:animated:)
    override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {

    }
}

This works but you have to deal with reimplementing many of the scroll views behaviors and methods that you normally get for free. Since scrollRectToView and scrollToView both use setContentOffset you also have to reimplement these if you want them to work.