4
votes

I have a large UITableViewCell that has 2 custom UIViews in it. One of them should allow interaction (selection and deselection), and one of the views should not allow interaction.

I know I can enable or disable interaction on the entire cell, or on each of the subviews, but if I disable interaction on the UIView the cell still becomes selected or deselected.

Is there a way to, for example, allow touches on the top half of the cell and not the bottom?

Here's a screenshot of the UITableViewCell I described

enter image description here

5

5 Answers

2
votes

you can forward or cancel touches with hit test

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        if let hitView = super.hitTest(point, withEvent: event) {
            if hitView.isKindOfClass(CustomSubviewClass) {
                return nil
            } else {
                return hitView
            }
        } else {
            return nil
        }
    }
2
votes

I would say that there is a flaw in your implementation, or that you have an X/Y problem. Consider splitting the single cell into two separate cells, and disabling selection on the bottom cell.

If you must move forward with this implementation, consider disabling selection on the entire cell. Put a gesture recognizer in the top subview, and on touch manually call didSelectRowAtIndexPath:.

1
votes

I totally agree with JAL, you need to review all your cell structure, but I know also that sometimes and in some cases it's impossible doing refactor.

So , I think this, suppose you have a CustomTableViewCell composed by two views , named for example view1 and view2:

The code:

class MyView1 : UIView {
    var isTouched : Bool = false
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.isTouched = true
        self.nextResponder()?.touchesBegan(touches, withEvent: event)
    }
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.isTouched = false
        self.nextResponder()?.touchesEnded(touches, withEvent: event)
    }
}
class MyView2 : UIView {
    var isTouched : Bool = false
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.isTouched = true
        self.nextResponder()?.touchesBegan(touches, withEvent: event)
    }
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.isTouched = false
        self.nextResponder()?.touchesEnded(touches, withEvent: event)
    }
}
class CustomTableViewCell: UITableViewCell {
    @IBOutlet weak var myExampleLabel: UILabel!
    @IBOutlet weak var view1: MyView1!
    @IBOutlet weak var view2: MyView2!

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        print("situation: myView1 : \(self.view1.isTouched) myView2: \(self.view2.isTouched)")
        var responder : UIResponder! = self
        while responder.nextResponder() != nil {
           responder = responder.nextResponder()
           if responder is SimpleTableView.ViewController { // the name of your tableView delegate class
                let vc = responder as! ViewController
                let indexPath = vc.tableView.indexPathForCell(self)
                vc.tableView(vc.tableView, didSelectRowAtIndexPath: indexPath!)
            }
        }
        super.touchesBegan(touches, withEvent: event)
    }
}

To the other side, where you have for example a UIViewController with a UITableView:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    @IBOutlet var tableView: UITableView!
    var items: [String] = ["We", "Heart", "Swift", "So", "Much"]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.estimatedRowHeight = 140.0
        self.tableView.registerNib(UINib(nibName: "CustomTableViewCell", bundle:nil), forCellReuseIdentifier: "CustomTableViewCell")
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.items.count;
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("CustomTableViewCell", forIndexPath: indexPath) as! CustomTableViewCell
        cell.myExampleLabel.text = self.items[indexPath.row]
        return cell
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let cell = tableView.cellForRowAtIndexPath(indexPath) as! CustomTableViewCell
        print("row = %d",indexPath.row)
        if cell.view1.isTouched { 
           print("tableView: touched view 1") 
           // do whatever you want with cell.view1
           // if you want you can disable cell.view2.userInteractionEnabled
        }
        else {
            print("tableView: touched view 2")
            // same thing for cell.view2
        }
    }
}

Some important things:

Don't forget to set the correct Custom Class for the two views like explained in this picture:

enter image description here

0
votes

One way to make this work is to bypass the UITableViewDelegate methods, and use an IBAction with a custom callback handler on the cell.

  1. Set the Selection type for the cell to None to prevent the cell from becoming selected / deselected when you tap on it.
  2. Make sure your UITableViewDelegate does not implement tableView:didSelectRowAtIndexPath.
  3. Add a button view into the cell and position it so it covers the area you want the user to interact with.
  4. Add @IBAction func buttonTapped(sender: UIButton) in the UITableViewCell sub-class.
  5. Add a closure variable to the cell. Call the closure when the button is tapped.
  6. In the tableView:cellForRowAtIndexPath: delegate method set the closure on the cell to handle the call.

Example UITableViewCell subclass:

class MyTableViewCell: UITableViewCell {

    var selectionHandler: ((Void) -> Void)?

    override func prepareForReuse() { 
        // Remove the closure when the cell is recycled.
        selectionHandler = nil
    }

    @IBAction func buttonTapped(sender: UIButton) {
        // Call the closure when the button is tapped.
        selectionHandler?()
    }
}

In your UITableViewController:

class MyTableViewController: UITableViewController {

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        let cell = ....

        // Set the closure to be called when the button is tapped.
        // Using weak self here to prevent a retain cycle.
        cell.selectionHandler = { [weak self] in
            self?.selectedCellAtIndexPath(indexPath)
        }

        return cell
    }

    // We are not using didSelectRowAtIndexPath
    // override func tableView(tableView: UITableView, didSelectRowAtIndexPath: NSIndexPath) {
    // }
}

I have not tested this code so it may not compile, although it should serve to illustrate the concept.

0
votes

From table view property editor, set Selection value to No Selection or do it programmatically. Disable interaction on each of the subviews, except the subview you want action. Pass indexPath row value to that subview as tag, maybe. Or subclass that subview from UIView to store indexPath info. Next, implement tap gesture to that subview and perform your desire action on tap gesture recognize method. Hope these should do what you want.