0

I have a TableView with cells that when pressed anywhere in the cell, it adds a checkmark on the right. I only want the checkmark to show up if the cell is tapped on the right side. Here's the pertinent section of code from the TableViewController:

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) as! TaskCell

        let task = tasks[indexPath.row]
            cell.task = task


        if task.completed {
            cell.accessoryType = UITableViewCellAccessoryType.checkmark;
        } else {
            cell.accessoryType = UITableViewCellAccessoryType.none;
        }

        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: false)

        var tappedItem = tasks[indexPath.row] as Task
        tappedItem.completed = !tappedItem.completed
        tasks[indexPath.row] = tappedItem

        tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
    }

}

Is there a simple way to do that, or to do it using storyboard? My Swift skills leave a LOT to be desired. Any help would be appreciated! Thank you!

Bob F.
  • 93
  • 1
  • 11

2 Answers2

3

Instead of the built-in checkmark accessory type, why not provide, as accessory view, an actual button that the user can tap and that can display the checkmark? The button might, for example, display as an empty circle normally and as a circle with a checkmark in it when the user taps it.

Otherwise, you're expecting the user to guess at an obscure interface, whereas, this way, it's perfectly obvious that you tap here to mark the task as done.

Example:

enter image description here

To accomplish that, I created a button subclass and set the accessoryView of each cell to an instance of it:

class CheckButton : UIButton {
    convenience init() {
        self.init(frame:CGRect.init(x: 0, y: 0, width: 20, height: 20))
        self.layer.borderWidth = 2
        self.layer.cornerRadius = 10
        self.titleLabel?.font = UIFont(name:"Georgia", size:10)
        self.setTitleColor(.black, for: .normal)
        self.check(false)
    }
    func check(_ yn:Bool) {
        self.setTitle(yn ? "✔" : "", for: .normal)
    }
    override init(frame:CGRect) {
        super.init(frame:frame)
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

The title of the button can be the empty string or a checkmark character, thus giving the effect you see when the button is tapped. This code comes from cellForRowAt::

    if cell.accessoryView == nil {
        let cb = CheckButton()
        cb.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        cell.accessoryView = cb
    }
    let cb = cell.accessoryView as! CheckButton
    cb.check(self.rowChecked[indexPath.row])

(where rowChecked is an array of Bool).

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I would love to do exactly that, in fact I really like the circles rather than boxes, but unfortunately I don't know how to do that programmatically. – Bob F. Mar 27 '17 at 00:27
  • Improved code to make the checkmark button more encapsulated. – matt Mar 27 '17 at 14:54
  • My weak programming abilities are haunting me again! I'm getting 3 errors under cellForRowAt: 1. In the cb.addTarget line, "Use of unresolved identifier 'touchUpInside'" 2. In the cb.addTarget line, "Use of unresolved identifier 'buttonTapped'" 3. Value of type 'ListTableViewController' has no member 'rowChecked'" I guess I need to set up the rowChecked array somewhere? I'm not sure how to address errors 1 and 2 though. – Bob F. Apr 20 '17 at 21:00
  • You'd need to have a method `func buttonTapped` too. – matt Apr 20 '17 at 22:51
  • I just ordered your book! Hopefully it helps me figure this out. Thanks matt, I think you've helped me on here before; you're a great resource! – Bob F. Apr 21 '17 at 15:20
1

You will have to define your own accessory button, and handle its own clicks.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) as! TaskCell

    let task = tasks[indexPath.row]
        cell.task = task

    let checkButton = UIButtonSubclass()
    ...configure button with your circle and check images and a 'selected property'...
    checkButton.addTarget(self, action:#selector(buttonTapped(_:forEvent:)), for: .touchUpInside)
    cell.accessoryView = checkButton
    checkButton.selected = task.completed //... this should toggle its state...

    return cell
}

func buttonTapped(_ target:UIButton, forEvent event: UIEvent) {
    guard let touch = event.allTouches?.first else { return }
    let point = touch.location(in: self.tableview)
    let indexPath = self.tableview.indexPathForRow(at: point)
    if let task = tasks[indexPath.row] {
        task.completed = !task.completed
    }
    tableView.reloadData()   //could also just reload the row you tapped
}

Though, it has been noted that using tags to detect which row was tapped is dangerous if you start to delete rows. You can read more here https://stackoverflow.com/a/9274863/1189470

EDITTED Removed the reference to tags per @matt

Community
  • 1
  • 1
dmorrow
  • 5,152
  • 5
  • 20
  • 31
  • Indeed. Then don't show this use of tags, since indeed it _is_ dangerous. Here's how I do it (this would be in your `buttonTapped`): `var r : UIView = sender; repeat {r = r.superview!} while !(r is UITableViewCell); let ip = self.tableView.indexPath(for: r as! UITableViewCell)!` Now we have the desired index path and can take its `row` as desired. – matt Mar 27 '17 at 00:49
  • Fair point @matt - thought I'm less of a fan of iterating through UIKit view hierarchies, so I got less lazy and typed how I typically handle it! – dmorrow Mar 27 '17 at 00:57
  • Equally fair point. But gosh, I iterate thru view / responder hierarchies all the time. What on earth are they for if you can't do this? It's nothing but a linked list after all. Super fast, super efficient, tried and true. – matt Mar 27 '17 at 01:12
  • Thanks dmorrow! I'm getting the error "Use of unresolved identifier 'event'". Any idea how to fix that? – Bob F. Apr 23 '17 at 14:58
  • @BobF. - sorry, see my edits. I was using the wrong signature for `buttonTapped` – dmorrow Apr 23 '17 at 23:13
  • @dmorrow Ok, that fixed the 'event' error, but now there's a "Value of optional type 'indexPath?' not unwrapped" error in the line "if let task = tasks[indexPath.row] {". Is there a way to guard that? Or should I address that another way? – Bob F. Apr 25 '17 at 20:53