melh0n
melh0n

Reputation: 67

How to add UITapGestureRecognizer to an image within UICollectionViewCell

Each of my cell's contain one imageView. The goal is to have it so that any time the cell (or imageView) is tapped or swiped the image changes with the corresponding tap or swipe animation. (The tap animation shrinks and then expands, while the swipe animation rotates the imageView 180 degrees)

At first I set up my tap functionality in my collection view's didSelectItemAt method and this worked fine. But, when I tried to add the swipe functionality I found that any user interaction with the cell would trigger the same tap animation.

SO, now I'm trying to set up the tap functionality in cellForItemAt. However, the tap selector for some reason is not being called. Any ideas why?

I am using a UICollectionViewController. Xcode 12, swift 5.

EDIT

Following @Sandeep Bhandari's suggestions I am now implementing a protocol to communicate between the cell (which holds a strong ref to the tap gesture) and UICollectionViewController. However, selector is still not being called.

GameTileCell

@objc protocol GameTileCellProtocol {
     func didTapImageView(for cell: GameTileCell)
}

class GameTileCell: UICollectionViewCell {

     // other properties
     var gesture: UITapGestureRecognizer?
     weak var delegate: GameTileCellProtocol?

    override init(frame: CGRect) {
        super.init(frame: frame)
        // call configure cell method
        
        gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        gameTileImageView.isUserInteractionEnabled = true
        gameTileImageView.addGestureRecognizer(gesture!)
        
    }
    
    // storyboard init
    // configure cell method defined
    
    @objc func handleTap(_ sender: UITapGestureRecognizer) {
        self.delegate?.didTapImageView(for: self)
        print("handleTap")
    }
}

UICollectionViewController

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GameTileCell.reuseID, for: indexPath) as! GameTileCell
        setImageAndColor(for: cell)
        cell.delegate = self
        return cell
    }
// other methods

extension GameTileCVC: GameTileCellProtocol {
func didTapImageView(for cell: GameTileCell) {
    UIView.animate(withDuration: 0.4) {
        cell.gameTileImageView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
    } completion: { (_ finished: Bool) in
        UIView.animate(withDuration: 0.4) {
            self.setImageAndColor(for: cell)
            cell.gameTileImageView.transform = CGAffineTransform.identity
        }
    }
    print(#function)
}

}

Upvotes: 0

Views: 1235

Answers (2)

Tolga Naziyok
Tolga Naziyok

Reputation: 1

If you want to add tap and swipe functionality to your cells, you need to define for each a separate gesturerecognizer:

let tap = UITapGestureRecognizer(target: self, action:#selector(tapAction(_:)))
let swipe = UISwipeGestureRecognizer(target: self, action:#selector(swipeAction(_:)))
view.addGestureRecognizer(tap)
view.addGestureRecognizer(swipe)

As to why your selector action is not being called: Have you set a breakpoint on this line?

if let indexPath = self.collectionView?.indexPathForItem(at: sender.location(in: self.collectionView)) {

I think the if might not turn out positive and thus leave your action without doing anything.

Upvotes: 0

Sandeep Bhandari
Sandeep Bhandari

Reputation: 20379

You are creating a tap gesture recognizer inside a cellForItemAt indexPath: IndexPath) method which means its a local variable inside this method. Scope of this gesture recognizer is only inside this function cellForItemAt indexPath: IndexPath) as soon as control leaves the function's scope gesture recognizer will be deallocated and hence touch wouldn't work.

You need to hold the strong reference to the tap gesture recognizer, hence I would suggest moving this tap gesture recognizer from your ViewController to GameTileCell instead.

So change your GameTiteCell implementation to something like

class GameTileCell: UICollectionViewCell {
    let gesture: UITapGestureRecognizer
    weak var delegate: GameTitleCellProtocol?
    //other declarations of yours

    override init(frame: CGRect) {
        super.init(frame: frame)
        gameTileImageView.isUserInteractionEnabled = true
        gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        self.gameTileImageView.addGestureRecognizer(gesture)
    }

    @objc func handleTap(_ sender: UITapGestureRecognizer) {
        self.delegate?.didTapImageView(for: self)
    }
}

Declare a protocol to communicate between your cell and your ViewController. Lets call it as

@objc protocol GameTitleCellProtocol {
    func didTapImageView(for cell: GameTileCell)
}

Finally, make your ViewController the delegate of your GameTileCell in cellForRowAtIndexPath

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GameTileCell.reuseID, for: indexPath) as! GameTileCell
    setImageAndBgColor(for: cell)
    cell.delegate = self    
    return cell
}

Finally make your ViewController confirm to GameTitleCellProtocol protocol

extension YourViewController: GameTitleCellProtocol {
    func didTapImageView(for cell: GameTileCell) {
        UIView.animate(withDuration: 0.4) {
            cell.gameTileImageView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
        } completion: { (_ finished: Bool) in
            UIView.animate(withDuration: 0.4) {
                self.setImageAndBgColor(for: cell)
                cell.gameTileImageView.transform = CGAffineTransform.identity
            }
        }
    }
}

Upvotes: 2

Related Questions