Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIScrollView scrollRectToVisible:animated: not taking rect into account on iOS7

[self.scrollView scrollRectToVisible:rect animated:YES];

Does anyone have a clue of why this works perfectly fine on iOS6.1 and on iOS7.0.4 always scrolls to the UITextField that has become firstResponder no matter what kind of rect I send as an argument?

CGRect rect = CGRectMake(0, self.scrollView.frame.size.height - 1, 320, 1);
[self.scrollView scrollRectToVisible:rect animated:YES];

This code will scroll the UIScrollView to its bottom when the keyboard is showed due to a UITextField inside the UIScrollView has become first responder on iOS6.1 but on iOS7.0.4 it is scrolled so that the UITextFiled is visible instead.

As I figure this, the UIScrollView in the iOS7 SDK no matter what, autoscrolls to whatever has become the first responder inside of it when scrollRectToVisible:animated: is called.

like image 307
Henrik Lineholm Avatar asked Jan 29 '14 14:01

Henrik Lineholm


5 Answers

I suspect that most of you developers are using scrollRectToVisible:Animated: in conjunction with system keyboard notifications as explained in the Apple Docs here. For me the sample code provided by Apple didn't work (well, only half of it did).

Putting the method call inside a dispatch block fixed the problem for me:

dispatch_async(dispatch_get_main_queue(), ^{
    [self.scrollView scrollRectToVisible:rect animated:YES];
});

I don't fully understand why this works and I'm not sure if this is 100% safe but on the other hand it feels a lot safer than just delaying the call by 0.1 seconds as suggested in another answer by Rikkles.

I'm not an expert on threading issues (yet) but it seems like whatever hidden system method is overriding the scrolling behavior is already on the main queue when the UIKeyboardDidShowNotification is sent. So if we put our method call on the main queue as well it will be executed afterwards and therefor yield the desired effect. (But that's only a guess.)

like image 118
Mischa Avatar answered Nov 14 '22 06:11

Mischa


On iOS 8 (and possibly 7), the OS autoscrolls to the UITextField at the tail end of the runloop operation, just before it goes back to listening to user input. I haven't found any way to get in after the OS autoscroll and before the user input. Neither UIKeyboardWillShowNotification nor UIKeyboardDidShowNotification are hooks that will work.

However, what will always work is the good old trick of performing a selector after delay. Simply put the scrolling code in its own method, and call that method like this:

- (void)keyboardDidShow:(NSNotification*)aNotification {
  // ... all code to choose the view you want ...
  [self performSelector:@selector(moveToView:) withObject:visibleView afterDelay:0.1];
}

- (void)moveToView:(UIView *)aView {
  [self.scView scrollRectToVisible:aView.frame animated:YES];
}

And that will run after the OS autoscrolls, and you're golden.

like image 9
Rikkles Avatar answered Nov 14 '22 06:11

Rikkles


I met this problem before. Not an easy one, but boring for sure.

It was because I set contentSize to 0 (because you don't want it to scroll). And you should set at least 1.

[scrollView setContentSize: CGSizeMake(1, self.view.frame.size.height)];

I hope it's the solution ;)

like image 4
Tancrede Chazallet Avatar answered Nov 14 '22 07:11

Tancrede Chazallet


I found a solution to this problem, but it is not a pretty one. In order to scroll the scrollview to the desired location, u must register for both the keyboardWillShow and keyboardDidShow notifications. Then code to set the scrollview's insets is placed in the keyboardWillShowNotification's observer's selector and the code to scroll the scrollview to the desired location is placed in the keyboardDidShowNotification's observer's selector. Here is what I have:

Inside viewDidLoad:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];

Notification Methods:

- (void) keyboardWillShow: (NSNotification*) aNotification;
{
    NSDictionary* info = [aNotification userInfo];
    CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
    float kbHeight = kbSize.height < kbSize.width ? kbSize.height : kbSize.width;
    UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbHeight, 0.0);
    _scrollView.contentInset = contentInsets;
    _scrollView.scrollIndicatorInsets = contentInsets;
}

-(void)keyboardDidShow:(NSNotification*)notification
{
    CGRect aRect = CGRectMake(0, 0, _scrollView.frame.size.width, _scrollView.frame.size.height - _scrollView.frame.origin.y - self.scrollView.contentInset.bottom);
    CGRect scrollFrame = CGRectMake(self.loginView.frame.origin.x + self.loginButton.frame.origin.x, self.loginView.frame.origin.y + self.loginButton.frame.origin.y, self.loginButton.frame.size.width, self.loginButton.frame.size.height);
    if (!CGRectContainsRect(aRect, scrollFrame)) {
        [_scrollView scrollRectToVisible:scrollFrame animated:YES];
    }
}
like image 2
minimike08 Avatar answered Nov 14 '22 06:11

minimike08


I was following the Apple docs but with no success. Then I tried calling setContentOffset(_:animated:) on my scrollView, instead of scrollRectToVisible(_:animated:), and that made it work.

The code below scrolls to myView if it is hidden under keyboard, supposing you call keyboardWillShow function when you receive a UIResponder.keyboardWillShowNotification.

Swift 5

@objc private func keyboardWillShow(_ notification: Notification) {
    if let keyboardHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height {
            scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)

        let visibleViewFrame = myView.frame
        var scrollViewFrame = scrollView.frame
        scrollViewFrame.size.height -= keyboardHeight

        if !scrollViewFrame.contains(visibleViewFrame) {
            scrollView.setContentOffset(CGPoint(x: 0, y: visibleViewFrame.origin.y), animated: true)
        }
    }
}
like image 1
Michal Šrůtek Avatar answered Nov 14 '22 07:11

Michal Šrůtek