Detecting Drag-Into Touch Events in UIViews

Recently, I was working on a iOS app where I had several UIViews arranged on a screen and wanted to detect when the user drags a finger into one of the views. Seems like a straightforward thing to do, right?

I know about the UIControl class, which is a subclass of UIView, and that you can register handlers for a wide range of events by using the addTarget:action:forControlEvents: method. I looked through the list of events and found one called UIControlEventTouchDragEnter which sounded like exactly what I was looking for.

To test it out, I dropped a UIControl on the view, registered an event handler, and let it rip. The code looked something like this:


    ...
    _dragTestView = [UIControl new];
    _dragTestView.backgroundColor = [UIColor grayColor];
    [self addSubview:_dragTestView];
    [_dragTestView addTarget:self action:@selector(handleDragEvent:forEvent:) forControlEvents:UIControlEventTouchDragEnter];
    ...

- (IBAction)handleDragEvent:(id)sender forEvent:(UIEvent*)event {
    DDLogVerbose(@"DRAG ENTERED!");
}

The results were disappointing. Touching outside the control and dragging my finger into it did not result in my event handler being called. However, if I touched inside the control, then dragged outside and back in, the event handler did get called. Not exactly what I was looking for.

Next, I tried a different control event: UIControlEventTouchDragInside. The results were different, but still not what I was looking for. Dragging my finger around inside the control resulted in many events firing during the drag, but I still could not touch outside the control, drag my finger inside, and get an event to fire.

After doing a bit of research, I found that UIKit is designed so that a view will not receive touch events for any touch that did not begin in that view. This led me to change my approach a bit.

My next idea was to try setting up the touch handler on the outer container view and then checking with each subview to see if the touch coordinates are inside that view. I could have used the same UIControlEvents that I just experimented with, but my outer view was not a subclass of UIControl.

Fortunately, UIView has another way of getting touch events. There are four methods: touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, and touchesCancelled:withEvent:, which you can override to add custom handling for these events.

I added an implementation of touchesMoved:withEvent to my outer container view, and this time added several subviews that were just UIViews. In the touchesMoved handler, I iterated over the list of subviews and called the pointInside:withEvent: method on each of them to determine if the point associated with the touch event landed inside the view.

To my surprise, this didn’t work, either. The pointInside method checks the point against the views bounds, not its frame. The bounds of a view are relative to its own coordinate system, not the coordinate system of its superview. So pointInside only works if the view is positioned at the upper left corner of its superview.

To fix this, I made a subclass of UIView, overrode the pointInside:withEvent: method, and put in my own implementation that checked the points coordinates within the views frame. It looked like this:


- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    return CGRectContainsPoint(self.frame, point);
}

Then, in the superview’s touchesMoved:withEvent:, I iterate over all the subviews, call the method above, and get a simple YES or NO which tells me if the point is inside each view.


- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UIView *row in self.rows) {
        for (UITouch *touch in touches) {
            if ([row pointInside:[touch locationInView:self] withEvent:event]) {
                // Do something here!
            }
        }
    }
}

The final result looks like this.
Drag Test
Overall, dealing with touch events in iOS is not too bad. The UIKit classes are quite flexible and powerful. Also, I didn’t get into it in this post, but I believe the same thing could have been accomplished with Gesture Recognizers.

Conversation
  • Brian Croom says:

    Hey! Instead of overriding -pointInside:withEvent: you should consider instead consider passing the child view into the touch’s -locationInView: method which will do the required coordinate system conversion for you!

  • Comments are closed.