0

I'm trying to implement a long press triggered multiple cell selection on a UITableView. Once the multiple selection has triggerred, the selection and deselection must continue with tableview's didSelectRowAt and didDeselectRowAt delegates.

Everything works fine except when I want to deselect the first selected cell (the long press triggered one).

What I want

I want to be able to deselect the first selected cell with tableview's didDeselectRowAt delegate.
But it can't be deselect unless it long pressed again.
As far as I understand, the UILongPressGestureRecognizer interferes with the touch event delivery mechanism, as a result didSelectRowAt or didDeselectRowAt is not getting called on the first selected cell. But they do get called when the other cells are tapped.

What I don't want

Is to add another tap recognizer along with the long press recognizer.

How do I...

How would I achive this selection behaviour on the table view by using only its delegate?

Additional info: To manage and keep track of selections, I implemented a simple SelectionTracker class along with its SelectionTrackerDelegate. See the implementation code below.

Here is a minimum reproducible example implementation that I extracted from the original project.

SimpleListItem.swift

import Foundation

struct SimpleListItem {
    private(set) var id: Int // Used for selection tracking
    private(set) var title: String

    init(id: Int, title: String) {
        self.id = id
        itle = title
    }
}

SelectionTracker.swift

import Foundation

protocol SelectionTrackerDelegate {
    func selectionWillChange<K: Hashable>(oldSelection: Set<K>, newSelection: Set<K>)
    func selectionDidChanged<K: Hashable>(key: K, selected: Bool)
    func selectionDidEnd()
}

open class SelectionTracker<K: Hashable> {
    
    var delegate: SelectionTrackerDelegate?
    
    open var selection: Set<K> = Set<K>() {
        willSet {
            delegate?.selectionWillChange(oldSelection: selection, newSelection: newValue)
        }
    }
    
    open func changeSelection(_ k: K) {
        if selection.contains(k) {
            selection.remove(k)
            delegate?.selectionDidChanged(key: k, selected: false)
            if selection.count < 1 {
                delegate?.selectionDidEnd()
            }
        } else {
            selection.insert(k)
            delegate?.selectionDidChanged(key: k, selected: true)
        }
    }
    
    open func selectAll(_ items: [K]) {
        items.forEach({ item in
            if !selection.contains(item) {
                selection.insert(item)
                delegate?.selectionDidChanged(key: item, selected: true)
            }
        })
    }
    
    open func deselectAll() {
        selection.forEach({ item in
            selection.remove(item)
            delegate?.selectionDidChanged(key: item, selected: false)
        })
        if selection.count < 1 {
            delegate?.selectionDidEnd()
        }
    }
    
    open func isSelected(_ k: K) -> Bool {
        return selection.contains(k)
    }
    
    open func hasSelection() -> Bool {
        return !selection.isEmpty
    }
    
    open func selectedCount() -> Int {
        return selection.count
    }
}

ViewController.swift

import UIKit

class ViewController: UIViewController, UITableViewDelegate,
UITableViewDataSource, SelectionTrackerDelegate {
    
    @IBOutlet weak var tableview: UITableView!
    
    private let identifier = "simpleTableCell"
    private var data: [SimpleListItem] = []
    
    private lazy var selectionTracker: SelectionTracker<Int> = {
        let st = SelectionTracker<Int>()
        st.delegate = self
        return st
    }()
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableview.register(UITableViewCell.self, forCellReuseIdentifier: identifier)
        
        let lpgr = UILongPressGestureRecognizer(target: self, action: #selector(onLongPress(_:)))
        lpgr.cancelsTouchesInView = false
        iew.addGestureRecognizer(lpgr)
        
        for i in 1...20 {
            let item = SimpleListItem(id: i-1, title: "Simple cell \(i)")
            data.append(item)
        }
        tableview.reloadData()
    }
    
    
    // MARK: - TableView Data soruce delegate
    func tableview(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }
    
    func tableview(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableview.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
        cell.textLabel?.text = data[indexPath.row].title
        ccessoryType = selectionTracker.isSelected(data[indexPath.row].id) ? .checkmark : .none
        return cell
    }
    
    // MARK: TableView delegate
    func tableview(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("didSelectRowAt \(indexPath)")
        tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
    }
    
    func tableview(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        print("didDeselectRowAt \(indexPath)")
        tableView.cellForRow(at: indexPath)?.accessoryType = .none
    }
    
    
    // MARK: - Long press handling
    @objc func onLongPress(_ longPressRecognizer: UILongPressGestureRecognizer) {
        if longPressRecognizer.state == .began {
            let touchPoint = longPressRecognizer.location(in: tableview)
            if let ip = tableview.indexPathForRow(at: touchPoint) {
                print("onLongPress: IndexPath \(ip)")
                // Trigger multiple cell selection
                selectionTracker.changeSelection(data[ip.row].id)
            }
        }
    }
    
    
    // MARK: - Selection delegate
    func selectionWillChange<K>(oldSelection: Set<K>, newSelection: Set<K>) where K : Hashable {
        // TODO: Handle selection change
        if oldSelection.count == 0, newSelection.count == 1 {
            // ie. show a menu if it is first selection
        }
    }
    
    func selectionDidChanged<K>(key: K, selected: Bool) where K : Hashable {
        // Change the cell's selected state
        if let k = key as? Int,
            let index = getIndex(for: k),
            = tableview.cellForRow(at: IndexPath(row: index, section: 0)) {
                print("selectionDidChanged \(index)")
                cell.setSelected(selected, animated: true)
                cell.accessoryType = selected ? .checkmark : .none
            }
    }
    
    func selectionDidEnd() {
        // TODO: Handle selection end; ie. hide the menu
    }
    
    private func getIndex(for key: Int) -> Int? {
        return data.firstIndex(where: { $0.id == key })
    }
}

NOTE 1
The table view is configured with multiple selection and the view controller delegated for data source and table view delegate in the interface builder.

NOTE 2
I also tried to implement long press triggered multiple cell selection using UITableView.setEditing() method. It did entered the multi selection mode, some check boxes appeared on the left side of the cell, however when I tap to select the cells, none of the cells were getting checked with the checkmark.

Kozmotronik
  • 2,080
  • 3
  • 10
  • 25

0 Answers0