10

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.

morningstar
  • 8,952
  • 6
  • 31
  • 42
  • 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 Oct 13 '11 at 07:58
  • 1
    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 Apr 01 '15 at 17:57

5 Answers5

4

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).

Aberrant
  • 3,423
  • 1
  • 27
  • 38
  • This doesn't work because the functions are not called in an order that would make this work. The first time you tap on a textField `setContentOffset` is called first and `blockingTextViewScroll` is not set to `true` until after that. The solution only sort of works after that because `blockingTextViewScroll` will be `true` but it will result in unexpected behavior. – B Roy Dawson Dec 11 '20 at 14:51
2

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.

Mindaugas
  • 137
  • 1
  • 7
  • Looks like this may have been voted down, but it is a decent solution. I found the same: a delay of zero before executing a scrollRectToVisible will make this work, as it schedules it after the scroll view's automatic behavior. using GCD's dispatch_after is cleaner and more modern then performSelector:WithObject:afterDelay though. – Michael McGuire Apr 01 '15 at 17:56
0

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.

B Roy Dawson
  • 611
  • 1
  • 6
  • 18
0

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.

Akshay
  • 5,747
  • 3
  • 23
  • 35
  • Nope. For thoroughness I tried every combination of disabling userInteractionEnabled on both the scroll view and the UITextField, and placing those calls before and after scrollRectToVisible or becomeFirstResponder, or before and after both. Disabling userInteractionEnabled on the UITextField generally made the keyboard not appear, even if it was enabled when I called becomeFirstResponder. Setting it on the scroll view didn't seem to have any effect. – morningstar Oct 12 '11 at 01:23
0

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)

  • "Try changing the view autoresizing" Which view? The scroll view? Something else? I'm probably not gonna go there. I had to set all the autoresizing masks to very specific settings to get all the scroll, zoom, and tilt to work. "The other thing you can try to change the scrollView size first and then apply the scrollRectToVisible: change." Did that. – morningstar Oct 12 '11 at 18:03
  • If you know something about autoresizing maybe you can see [my other question](http://stackoverflow.com/questions/7681765/scrolling-zooming-uiscrollview-and-interface-orientation-rotation-how-to-use-au). – morningstar Oct 13 '11 at 08:08