Whenever you have a modal element that outside touches should cancel a few things need to happen:
- Touches inside the modal element should behave normally
- Outside touches should not trigger touch events on tappable elements
- Outside touches should trigger a handler to close the modal
In my case I was swiping a UITableViewCell and revealing a controls view that should close if any other UITableViewCell is tapped.
hitTest:withEvent – The easiest method
Touch events bubble down from the root view rather than up from the target view which makes this a bit easier. On whichever view needs to capture the touch events, we need to override hitTest:withEvent: to return nil and call a function whenever a modal is open and a view other than the modal is tapped. I overrode this method in my UITableView’s superview.
A naive implementation looks like this:
// NOTE: skip to the next code example for full implementation -(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent*)event { UIView *hit_view = [super hitTest:point withEvent:event]; //only perform these checks when a modal is actually open BOOL is_modal_open = (BOOL)self.currentlyRevealedCell; if (is_modal_open) { // For parents category see pdenya.com/g/uiview_parents if (!hit_view || ![hit_view parents:[RevealedView class]]) { //whatever method you need to call when something outside the modal is touched [[self currentlyRevealedCell] snapBack]; //return nil so we don't pass the event through hit_view = nil; } } return hit_view; }
Gotcha
The only problem with this method is that hitTest:withEvent: is called 3 times per tap so our flow looks something like:
- Handle first tap correctly
- See that modal is no longer open and pass 2nd and 3rd tap through
To correct this we need to make sure is_modal_open continues to return YES until after that 3rd hitTest gets called and make sure the handler only gets triggered once. Full method below.
-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent*)event { UIView *hit_view = [super hitTest:point withEvent:event]; //only perform these checks when a modal is actually open BOOL is_modal_open = (self.currentlyRevealedCell || self.isSnappingBack); if (is_modal_open) { // parents inlined below for speed and clarity if (!hit_view || ![hit_view parents:[RevealedView class]]) { // check to make sure this function wasn't called already if (self.currentlyRevealedCell) { //whatever method you need to call when something outside the modal is touched [[self currentlyRevealedCell] snapBack]; } //return nil so we don't pass the event through hit_view = nil; } } return hit_view; } // Returns a superview of a particular class or nil if no match is found // Available at https://pdenya.com/g/uiview_parents but inlined for clarity -(UIView *)parents:(Class)class_name { UIView *s = self.superview; while (![s isKindOfClass:class_name]) { if (s.superview) { s = s.superview; } else { return nil; } } return s; }