1

There must be something I am not getting about KVO, I am trying to scroll a UITableView by scrolling a UIScrollView, the offset translations are correct but the UITableView scrolling occurs after the UIScrollView has finished scrolling.

Here is my observing code:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {

    AILog(@"observeValueForKeyPath:%@ ofObject:%@", keyPath, NSStringFromClass([object class]));

    static BOOL isObservingContentOffsetChange = NO;

    if ([keyPath isEqualToString:@"contentOffset"]) {
        if ([object isEqual:self.scrollView]) {
            if (isObservingContentOffsetChange) {
                return;
            }
            isObservingContentOffsetChange = YES;

            CGPoint offset = [[change valueForKey:NSKeyValueChangeNewKey] CGPointValue];

            NSDate *offsetDay = [self dayForScrollViewOffset:offset];
            if (offsetDay) {
                AILog(@"offset (%.2f, %.2f)", offset.x, offset.y);
                AILog(@"offsetDay %@", [df stringFromDate:offsetDay]);
                NSIndexPath *offsetIndexPath = [self indexPathForDay:offsetDay];
                AILog(@"offsetIndexPath (%d, %d)", (int)offsetIndexPath.section, (int)offsetIndexPath.row);

                if (offsetIndexPath) {
                    CGRect rect = [self.tableView rectForSection:offsetIndexPath.section];
                    [self.tableView setContentOffset:CGPointMake(0, rect.origin.y) animated:YES];
                    AILog(@"----------------------------------------------");
                }
            } else {
                AILog(@"nil offset day");
            }
        }
        isObservingContentOffsetChange = NO;
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

And here is a console log which show this is working as expected:

014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1526.50, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Sunday, 19/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (2, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1520.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Sunday, 19/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (2, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1514.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Sunday, 19/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (2, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1508.50, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Sunday, 19/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (2, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1503.50, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Sunday, 19/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (2, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1498.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Sunday, 19/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (2, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1493.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Sunday, 19/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (2, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1488.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Sunday, 19/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (2, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1483.50, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1479.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1474.50, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1470.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1466.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1462.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1458.50, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1454.50, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1450.50, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> ----------------------------------------------
2014-10-21 17:44:53 +0000 : BPTimeViewController --> observeValueForKeyPath:contentOffset ofObject:UIScrollView
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offset (1447.00, 0.00)
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetDay Saturday, 18/10/2014
2014-10-21 17:44:53 +0000 : BPTimeViewController --> offsetIndexPath (3, 0)

But the UITableView starts scrolling only after all those content offsets calculation, when UIScrollView has finished decelerating (and there is nothing in UIScrollView delegate methods). Do you know why so?

Cœur
  • 37,241
  • 25
  • 195
  • 267
Fabrizio Prosperi
  • 1,398
  • 4
  • 18
  • 32
  • 6
    Is there a reason you wouldn't just use the `UIScrollViewDelegate` methods? Like `- scrollViewDidScroll`, for example? – i_am_jorf Oct 21 '14 at 18:00
  • Yes there is, and this is not the point. I need and want to use KVO and I asked this question not another. Thanks. – Fabrizio Prosperi Oct 21 '14 at 18:02
  • 5
    sounds like it does exactly that: running synchronously and blocking your UI. – vikingosegundo Oct 21 '14 at 18:15
  • 6
    That's why it was a comment seeking clarification, not an answer. You're welcome. – i_am_jorf Oct 21 '14 at 18:19
  • 1
    Also, just an observation, but not all paths invoke the superclass method due to your use of early returns in some cases. Is this by design? – Paulw11 Oct 21 '14 at 18:40
  • @Paulw11, yes this is the way KVO works in my understanding of various examples on the web, I am observing a specific instance of UIScrollView, all the others can go to super. Here there is nothing regarding UIScrollView implementation or delegation, only observing and actions. – Fabrizio Prosperi Oct 21 '14 at 20:38
  • The documentation (https://developer.apple.com/library/ios/DOCUMENTATION/Cocoa/Conceptual/KeyValueObserving/Articles/KVOBasics.html) suggests you should always call super if the super class supports KVO – Paulw11 Oct 21 '14 at 20:42
  • Yes, I know what documentation states but calling super gives the infamous error: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled. Key path: contentOffset Observed object: ; layer = ; contentOffset: {0, 0}; contentSize: {2264, 60}> Change: { kind = 1; new = "NSPoint: {0, 0}"; } – Fabrizio Prosperi Oct 21 '14 at 20:53

1 Answers1

4

UITableView is a subclass of UIScrollView. When you animate scrolling of a UIScrollView, it does not use a CAAnimation to animate the change, unlike most other view animations. Instead, it uses a timer.

I was able to reproduce your problem and investigate. I believe your problem is that the timer is scheduled to run only in the run loop's default mode (kCFRunLoopDefaultMode), but when a user is dragging a scroll view (and when the scroll view is decelerating after the drag), the run loop is in UITrackingRunLoopMode. So the table view's scroll timer doesn't begin firing until the deceleration ends.

There is no documented (or otherwise obvious) way to get the table view's timer to run in other run loop modes, but this seems to work:

[UIView animateWithDuration:0.2 animations:^{
    self.tableView.contentOffset = CGPointMake(0, rect.origin.y);
}];

This worked for me in the simulator as iPhone 5 (7.0.3) and iPhone 6 (8.0).

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • 1
    Incidentally, you can simplify your KVO method using the `context` parameter. See [this answer](http://stackoverflow.com/a/11917449/77567). – rob mayoff Oct 21 '14 at 18:48
  • Animating the content offset like this can cause trouble for the table view depending on when it reuses cells, since it doesn't "know" what's on screen during this animation. (Cells could flicker, etc.) Why not just set the content offset without animation? – Jesse Rusak Oct 21 '14 at 20:32
  • Thank you very much, this is a very good explanation and good workaround, you nailed it! And thank you also for the context suggestion. Cheers ;) – Fabrizio Prosperi Oct 21 '14 at 20:33
  • @JesseRusak, I tried this and doesn't seem to have issues with reuse. The animation is needed to make it smooth, not jumping. – Fabrizio Prosperi Oct 21 '14 at 20:35