Question

I am working on a UI test framework (KIF-next) which runs in the main thread of the application. The basic process is:

  1. Perform some test logic.
  2. Spin the main loop for 0.1 seconds via runUntilDate:.
  3. Repeat.

This method works incredibly well. You can simulate click events, you can swipe across sliders, watch alert views animate in and out, scrollToRowAtIndexPath:. Everything works well except one specific scenario, dragging scroll views.

You can drag the scroll view up and down but when you release your finger, nothing happens. Moreover, touch events are no longer recognized by other views and all you can do is drag the scrollview around.

If this execution pattern ends and the code exits, the scroll views will do their bounce/deceleration, but this isn't an option as I am working in ocunit which performs sequential execution.

My questions: Why is this happening and what can be done to correct it?

I have two demos:

Demo 1: Block your whole application

With this code, you can test out your application and confirm that everything is fine until your scroll. After that, nothing works.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        while (YES) {
            [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
        }
    });
    return YES;
}

Demo 2: Block temporarily

If you wire this code to a button, you will see the same effect as above if your first tap the button and then scroll. Once the timer completes, you will see your application spring back to life.

- (IBAction)spinButtonClicked:(id)sender
{
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
}
Was it helpful?

Solution

The problem, as Dave pointed out, has to do with run loop modes. Applications will reside mainly in the kCFRunLoopDefaultMode mode but switch into UITrackingRunLoopMode when the user starts scrolling and back again when the scroll view finishes decelerating. This appears to be to prevent normal timers from firing during scrolling. (You can see this in Safari where animations stop updating during scroll.)

The mechanics appear to be as follows.

  1. The UIApplication has a stack to keep track of the current run loop mode requested by the application.
  2. When the application starts up, it calls GSEventRunModal which in turn calls CFRunLoopRunInMode with the current run mode for the application, kCFRunLoopDefaultMode.
  3. When the user starts swiping a scroll view, the scroll view specific pan gesture recognizer enters the Began state and triggers [[UIApplication sharedApplication] pushRunMode:UITrackingRunLoopMode requester:self].
  4. This method pushes the string onto the stack, sees that the run mode is different, and calls CFRunLoopStop on the run loop created by GSEventRunModal.
  5. GSEventRunModal loops and calls CFRunLoopRunInMode again, this time with UITrackingRunLoopMode.
  6. When deceleration completes, the gesture recognizer calls [[UIApplication sharedApplication] popRunMode:UITrackingRunLoopMode requester:self] and similar steps take place.

The problem with my long running function that calls runUntilDate: it never terminates so CFRunLoopStop has no effect. One approach to take would be to swizzle pushRunMode:requester: and popRunMode:requester: and do something similar with my own internal logic.

The approach I've actually taken, since I will not be receiving any actual user interaction, is to send the touch move event to the application and check to see what gestureRecognizers are on it. If there is a UIScrollViewPanGestureRecognizer that entered the Began phase, the remainder of the run loops for dragging are done with UITrackingRunLoopMode. Once the touch simulation is complete, I continue to run in that mode until scrollView.dragging == NO.

You can see it in action at https://github.com/bnickel/KIF/blob/kif-next/Additions/UIView-KIFAdditions.m#L368

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top