How can I enable selecting multiple collection view cells at once with multitouch?
Asked Answered
C

2

7

I am writing an iOS card game. I display the player's cards in a collection view. The player can select one or more cards by tapping on them, and then press a deal button to deal the selected cards.

I want to allow the user to use multiple fingers to select multiple cards at once. For example, if the user wants to select 2 cards, he just needs to tap the two cards at the same time, with two fingers, and they will both be selected. It seems like that by default, UICollectionView does not allow this. When I tap with 2 fingers, only one of the cards will be selected, even though the isMultipleTouchEnabled property in UIView is already set to true.

Note that I am not asking about how to allow a user to select multiple items in a collection view. I can and did already do that with allowsMultipleSelection = true. What I am asking is how to allow the user to select 2 cells with 2 fingers (or n cells with n fingers).

I found this question, but that seems to be about how to show a border around a cell when it is selected.

I also looked into the documentation of UICollectionView but I found no property that controls this.

Corner answered 26/1, 2019 at 14:29 Comment(2)
@Cristik you mean like a UITapGestureRecogniser? How can I figure out which cell is tapped given a set of coordinates?Corner
But how do I figure out which cell is tapped? @CristikCorner
B
7

First let's understand exactly what the problem is. The collectionView has a bunch of UIGestureRecognisers attached to it (for pan, touch, zoom, etc). Each recogniser has the same state machine of possible->recognised-> changed-> ended/failed. Each recogniser has a clear start beginning and end. Once the tap gesture has started in one location it is not going to begin in another location. When a person 1) touches down point A 2) touches down point B 3) touches up point A 4) touches up point B that the gesture completely ignores point B because it is "focused" on point A.

The second problem is that if you touch at two points at the exact same time the method of tapGesture.location(in: view) will give you the average of those two location.

However we solve this the first step is to disable the collectionView tapGesture - it is not doing what we want :

  self.collectionView.allowsMultipleSelection = true
  self.collectionView.allowsSelection = false;

Next we are going to add our own tap gestures to each cell individually. This is explicitly NOT recommend by apple ("You should always attach your gesture recognizers to the collection view itself—not to a specific cell or view."1) but it will work:

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
 ...
  cell.addGestureRecognizer(UITapGestureRecognizer.init(target: self, action: #selector(didTap(tapper:))))
  ...
  return cell;
}

@objc func didTap(tapper:UIGestureRecognizer) {
  if let cell = tapper.view as? UICollectionViewCell{
    if let index = collectionView.indexPath(for: cell) {
      if collectionView.indexPathsForSelectedItems?.contains(index) ?? false {
        collectionView.deselectItem(at: index, animated: true)
        cell.isSelected = false
      }else{
        collectionView.selectItem(at: index, animated: true, scrollPosition: [])
        cell.isSelected = true
      }
    }
  }
}
Ballonet answered 30/1, 2019 at 8:51 Comment(5)
Why are you using a UITapGestureRecognizer in didTap, when TouchDownGestureRecognizeractually inherits from UIGestureRecognizer? Also, I would like the gesture recogniser to trigger when touch up instead of touch down, just like a normal collection view will do.Corner
I tried the most correct solution and now I can't scroll my collection view and deselect my cells. I guess I need to implement some delegate methods to stop my own recogniser from interfering with the collection view's recognisers?Corner
Yeah, you are right. You can kinda get it to work if you set the delegate and return true for shouldRecognizeSimultaneouslyWith. But overall it is not a good solution. I'll remove it.Ballonet
After some more debugging, I found out that after I call selectItem, the indexPathsForSelectedItems remains empty. This is why I could not deselect the cells again. Any ideas why this happens?Corner
I removed the line that sets allowsSelection to false and it works! What is the purpose of that line anyway?Corner
C
3

You could add multiple gesture recognizers for the number of touches you want to support:

collectionView.allowsMultipleSelection = true
// allowing up to 5 finger touches, increase if you want for more :)
for i in 2...5 {
    let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
    gestureRecognizer.numberOfTouchesRequired = i
    gestureRecognizer.delegate = self
    collectionView.addGestureRecognizer(gestureRecognizer)
}

, and have the controller to look for the cells that were touched:

@objc private func handleTap(_ gestureRecognizer: UIGestureRecognizer) {
    // perform the action only after the touch ended
    guard gestureRecognizer.state == .ended else { return }

    for i in 0..<gestureRecognizer.numberOfTouches {
        let location = gestureRecognizer.location(ofTouch: i, in: collectionView)
        // if we have a cell at that point, toggle the selection
        if let indexPath = collectionView.indexPathForItem(at: location) {
            if collectionView.indexPathsForSelectedItems?.contains(indexPath) == true {
                collectionView.deselectItem(at: indexPath, animated: true)
            } else {
                collectionView.selectItem(at: indexPath, animated: true, scrollPosition: [])
            }
        }
    }
}
Companionate answered 30/1, 2019 at 21:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.