18

There have been similar questions before and I have referred to Capturing touches on a subview outside the frame of its superview using hitTest:withEvent: and Delivering touch events to a view outside the bounds of its parent view. But they do not seem to answer my particular problem.

I have a custom control with the following structure:

+-------------------------------+
|           UIButton            |
+-------------------------------+
|                          |
|                          |
|        UITableView       |
|                          |
|                          |
+------------------------- +

The custom control overrides the UIButton subclass and adds the UITableView as its subView. The idea is to have the whole control act like a dropdown. When the UIButton is pressed, the UITableView will dropdown and enable selection of a choice.

This all works fine with the hitTest overridden in UIButton as described in the Apple's Q&A link above, if the control is an immediate child of the topmost UIView. However, if the control is in another view hierarchy, the UITableView is not receiving touch events.

The following is the hitTest code used:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    // Convert the point to the target view's coordinate system.
    // The target view isn't necessarily the immediate subview
    let pointForTargetView = self.spinnerTableView.convertPoint(point, fromView:self) 

    if (CGRectContainsPoint(self.spinnerTableView.bounds, pointForTargetView)) {
        // The target view may have its view hierarchy,
        // so call its hitTest method to return the right hit-test view
        return self.spinnerTableView.hitTest(pointForTargetView, withEvent:event);
    }
    return super.hitTest(point, withEvent: event)
}

Edit:

I apologise for not having been able to look at the answers and check if they resolve the issue, due to some other tasks that require immediate attention. I shall check them and accept one that helps. Thanks for your patience.

Community
  • 1
  • 1
Rajesh
  • 15,724
  • 7
  • 46
  • 95

5 Answers5

5

As you stated:

However, if the control is in another view hierarchy, the UITableView is not receiving touch events.

Even if you made it working what if you have subviewed this control under another UIView??

This way we have a burden of converting points for the view hierarchy, my suggestion is that, as you can see in this control, it adds the tableview on the same hierarchy where the control itself is present. (e.g., it adds the table view on the controller where this control was added)

A part from the link:

-(void)textFieldDidBeginEditing:(UITextField *)textField
{
    [self setupSuggestionList];
    [suggestionListView setHidden:NO];

    // Add list to the super view.
    if(self.dataSourceDelegate && [self.dataSourceDelegate isKindOfClass:UIViewController.class])
    {
        [((UIViewController *)self.dataSourceDelegate).view addSubview:suggestionListView];
    }

    // Setup list as per the given direction
    [self adjustListFrameForDirection:dropDownDirection];
}

Hope that helps!

NeverHopeless
  • 11,077
  • 4
  • 35
  • 56
4

I think you should overridden pointInside implement:

class Control: UIButton {

    var dropdown: Bool = false

    override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
        if dropdown {
            return CGRectContainsPoint(CGRect(x: 0, y:0, width: self.bounds.width, self.bounds.height + spinnerTableView.frame.height), point)
        }
        return super.pointInside(point, withEvent: event)
    }
}

like this, superview call control's hittest when touch outside control bounds, it won't return nil.

But there are still a problem, if the control is in another view hierarchy, the UITableView may not receiving touch events.

I think this problem is normal,you can't decide other view's pointInside or not which under control, if the control superview hitTest return nil, the control hitTest method will not be called. Unless you overridden all view pointInside method which under the control, but it is unrealistic.

maquannene
  • 2,257
  • 1
  • 12
  • 10
3

I was thinking about the UIResponder , something like:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview

        var responder: UIResponder = self
        while responder.nextResponder() != nil {
            responder = responder.nextResponder()!
            if responder is UITableView {
                // Got UITableView
                break;
            }
        }
        let pointForTargetView = responder.convertPoint(point, fromView:self)
        if (CGRectContainsPoint(responder.bounds, pointForTargetView)) {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return self.responder.hitTest(pointForTargetView, withEvent:event);
        }
        return super.hitTest(point, withEvent: event)
}

If this method don't work, try to convertPoint with superview:

When you are sure that self.spinnerTableView is not nil and it is your table, do:

let pointForTargetView = self.spinnerTableView.superView.convertPoint(point, fromView:self) 
Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
  • Nope, the issue with the previous implementation was that the point was not correctly converted to the `UITableView` co-ordinate system. So even in the case above, `CGRectContainsPoint` does not identify the point to be within the `UITableView` bounds. – Rajesh Jun 18 '16 at 18:12
  • Thanks, but I need a generic way to do this. Irrespective of the number of hierarchical levels the control is in. – Rajesh Jun 18 '16 at 19:05
  • I don't understand if you have solve your problem or need only an high level syntax code to do it. As you have comment now , seems the issue was solved and you know how to do but you want a method to cover all possibilities superviews..can you explain better your comment thank you (sorry Im not english so maybe anything can be escaping..) – Alessandro Ornano Jun 18 '16 at 19:10
  • Sorry for the ambiguous comment. The updated answer did not solve the problem. From what I understand, you are suggesting to convert the point from the superview's co-ordinate system. This *may* work if the control is one level deeper. I need a working solution **AND** it should work in all cases. I will try to post a sample project so that you can try out your suggestions before posting an answer. – Rajesh Jun 18 '16 at 19:22
1

You need to subClass UIView,and use it as your UIViewController's root view.

class HitTestBottomView: UIView {

  override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

    if  let hitTestView  =    self.getHitTestButtonFrom(self){
        let pointForHitTestView = hitTestView.convertPoint(point, fromView: self)
        let pointForHitTestTableView = hitTestView.spinnerTableView.convertPoint(point, fromView: self)

        if CGRectContainsPoint(hitTestView.bounds, pointForHitTestView) ||  CGRectContainsPoint(hitTestView.spinnerTableView.bounds, pointForHitTestTableView){
            return hitTestView.hitTest(pointForHitTestView, withEvent: event)
        }
    }

    return super.hitTest(point, withEvent: event)
  }

  func getHitTestButtonFrom(view:UIView) ->HitTestButton?{
    if let testView = view as? HitTestButton {
        return testView
    }

    for  subView  in view.subviews {
        if let testView = self.getHitTestButtonFrom(subView) {
            return testView
        }
    }
    return nil
  }
}
wj2061
  • 6,778
  • 3
  • 36
  • 62
  • The idea was to use the control as a reusable component within any UIView hierarchy. Hence, using it as the root view is not a preferred solution. – Rajesh Jun 25 '16 at 06:26
1

You have to implement

func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool

for your UIButton and check UITableView bounds. Because hitTest first checks if this tap point is within your view's bounds then calls hitTest for your view. so you have to implement pointInside and return true for your bounds.

WWDC for advanced scroll views and touch handling techniques wwdc

farzadshbfn
  • 2,710
  • 1
  • 18
  • 38
  • This was the first thing I did. But as explained, the issue is in converting the point to the table view's co-ordinate system. – Rajesh Jun 25 '16 at 06:27