5
\$\begingroup\$

I have a UIScrollView full of vertically stacked subviews which I want the user to rearrange as they desire. I've implemented a UILongPressGestureRecognizer on my UIScrollView to successfully move the items around.

The more difficult part is when the user brings their finger to the edges of the scrolling area, the UIScrollView needs to scroll, while the gesture recognizer holds onto the floating reordering view. You get this behaviour for free in UITableView when you implement reordering. You can see an example of this when you edit your Safari bookmarks.

My implementation produces the same result, but I have a concern about it. Let me explain.

  1. With each update of the UILongPressGestureRecognizer I check the touch point to see if it is within 20 points of the visual boundaries of my UIScrollView.
  2. If we are within this boundary, I move the content offset by one pixel.
  3. To continue this process as long as the user's finger is within bounds, I repeat the method call.

    - (void)handleReorderGestureRecognizerChange:(id)sender
    {
        if (!self.reorderGestureRecognizerActive) return;
    
        // Scroll the scroll view
        BOOL scheduleRecursion = NO;
        if (sender == self.reorderGestureRecognizer && self.scrollViewIsAutomaticallyScrolling)
        {
    
        }
        else
        {
            // Determine the bounds of the scroll view
            const CGPoint superviewTouchPoint = [self.reorderGestureRecognizer locationInView:self.view];
            const CGPoint currentContentOffset = self.scrollView.contentOffset;
            CGPoint newContentOffset = currentContentOffset;
    
            // Determine if we are at the bottom
            const CGFloat scrollingBottomArea = (self.scrollView.bottom - 20);
            if (superviewTouchPoint.y > scrollingBottomArea)
            {
                const CGFloat maximumYContentOffset = self.scrollView.contentSize.height - self.scrollView.height;
                newContentOffset = CGPointMake(self.scrollView.contentOffset.x, fmin((self.scrollView.contentOffset.y + SMCompatibilitySinglePixelSize()), maximumYContentOffset));
            }
    
            // Determine if we are at the top
            if (superviewTouchPoint.y < 20)
            {
                newContentOffset = CGPointMake(self.scrollView.contentOffset.x, fmax((self.scrollView.contentOffset.y - SMCompatibilitySinglePixelSize()), -self.scrollView.contentInset.top));
            }
    
            // Schedule a change
            if (!CGPointEqualToPoint(currentContentOffset, newContentOffset))
            {
                self.scrollViewIsAutomaticallyScrolling = YES;
                self.scrollView.contentOffset = newContentOffset;
    
                scheduleRecursion = YES;
            }
            else
            {
                self.scrollViewIsAutomaticallyScrolling = NO;
            }
        }
        ...
        if (scheduleRecursion)
        {
            SM_run_block_after_delay(0.0005, ^{ // <- this hooks through to a dispatch_after call
                [self handleReorderGestureRecognizerChange:self];
            });
        }
    }
    

As I said, this works as expected, I am just very uncomfortable with the idea of continually scheduling this method from itself.

It is however the only way I can think of to make UIScrollView scroll continuously on its own.

As a reviewer would you say this is safe and efficient?

\$\endgroup\$
  • \$\begingroup\$ Is there a particularly good reason for not simply using a UITableView? \$\endgroup\$ – nhgrif Jun 28 '15 at 22:25
4
\$\begingroup\$

Technically this is not true recursion - dispatch_after(...) adds the block to whatever queue you assign it, so there's no chance of a stack overflow (although I'm not sure what will happen if you add too many blocks to the queue...) The block is handled by GCD so there's no chance of a retain cycle either. Plus, the block creation is inexpensive so it's not too inefficient (although the screen refreshes every 1/60 or ~0.017 seconds, so there is an inefficiency in having your timer interval at 0.0005s, about 33x faster than the refresh rate). Overall, however, this method is very hacky and there is a more conventional way to do this, using a CADisplayLink.

Two pointers for you:

  • Refactor the code that actually does the scrolling into a new method, instead of having it all in one place.
  • Use a CADisplayLink to call that method, thus managing the scrolling behavior.

Can provide code example if necessary.

Food For Thought

Unless you are challenging yourself, optimizing existing technology, or doing some research, it's good practice in general to skip reinventing the wheel. Why are you using a scroll view when you can get the functionality you desire, plus cell recycling and memory management for free with a UITableView?

In the context of your original question, I say it is far safer and more efficient to use technology that's already been proven to work. For example, here's one I use a lot, that supports long-pressing to reorder : https://github.com/hpique/HPReorderTableView

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.