Places To Check
There are several potential points of failure here, because what you're doing seems to look right. I have a sample Xcode project which has this working. (See it on GitHub.)
You can double check these things, to make sure everything is set up correctly:
- Your collection view's data source and delegate are wired up
- Your button and sample label have their outlet's connected correctly
- Your storyboard has the appropriate class and identifier set up
- Your selector is named correctly
It seems, based on what you've posted here that all of the above conditions are true, so let's talk a little more about what you're trying to do and see how all the pieces fit.
Why these Things Seem Ok
- If the data source and delegate were incorrectly set up, you wouldn't see your cells.
- If your outlets were incorrectly wired, you'd end up with crashes, because
IBOutlets
are force-unwrapped by default. (And that's how your posted code works.)
- Your storyboard seems to be set up correctly because, again, your cells wouldn't appear without the right set up.
- An incorrectly named selector wouldn't even compile.
Implications of Cells and Recycling
In general, this is a tricky problem to solve, because of the way UICollectionView
is designed. (The same applies to UITableView
.)
Your button is inside of a re-usable collection view cell, which means that you not only have to handle taps, but you need to know which index path in your data set the button is currently referring to.
Further, views that are placed inside an instance of UICollectionViewCell
are actually inside the collection view cell's contentView
, which makes it just a little more work to handle.
Finally, if your cells scroll on and offscreen, then you may end up attaching the action to your button multiple times as the cell is recycled. So, you need to account for that as well.
An Implementation Example
Let's work with a very simple custom cell, that has a single button in it:

The code for the button looks like this:
import UIKit
class CustomCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var updateButton: UIButton!
}
Really simple. Notice the IBOutlet
. That needs to be wired up in interface builder as well.

The last thing we need to do is set up the cell's reuse identifier so that we can access it in code. With the cell selected in Interface Builder, add an identifier to the cell:

Ok, that's all you need to do with the cell. We're ready to implement the view controller. In the view controller, we're going to set up a few things:
- A label to show that our button inside the cell was tapped
- An outlet for our collection view, so we can get the index path of the cell that contains the button. (If you're using a
UICollectionViewController
instead of aUIViewController
, this won't be necessary.)
- Some code to show our custom cell.
- Some code to handle the button tap.
Let's start with the IBOutlets
:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
@IBOutlet weak var lastTappedLabel: UILabel!
@IBOutlet weak var collectionView: UICollectionView!
We need to wire those up in Interface builder. While you're at it, also connect the collection view's data source and delegate to be the "File's Owner" if they aren't connected already. (Again, if you're using a UICollectionViewController
instead of aUIViewController
, this won't be necessary.)
Next, let's implement some code to show our custom cell, using UICollectionViewDataSource
. We need to show at least one section and at least one cell:
// MARK: - UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 36
}
When we dequeue the cell, we want to force downcast our dequeued cell to the custom cell class we defined earlier. Then we can access the update button to add an action to it.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "com.mosheberman.samples.cell", for: indexPath) as! CustomCollectionViewCell
cell.updateButton.addTarget(self, action: #selector(updateLabel(_:)), for: .touchUpInside)
return cell
}
Now we need to implement the method for accessing the index path of the cell:
// MARK: - Updating the Label
@objc func updateLabel(_ sender:UIButton)
{
print("\(sender) tapped. Update label called.")
guard let button = sender as? UIButton else
{
print("Sender isn't a button. Returning.")
return
}
guard let contentView = button.superview else
{
print("Button isn't inside a contentView. Returning.")
return
}
guard let cell = contentView.superview as? UICollectionViewCell else
{
print("View's superview isn't a UICollectionViewCell instance. Returning.")
return
}
guard let collectionView = self.collectionView else
{
print("Our collection view isn't wired up. Returning.")
return
}
guard let indexPathForCell = collectionView.indexPath(for: cell) else
{
print("The cell doesn't correspond to an index path. Maybe it's not a child of this collection view?")
return
}
self.lastTappedLabel.text = "Tapped button at \(indexPathForCell.item), \(indexPathForCell.section)"
}
This method handles all of the checks you need to handle the tap and determine which item was actually tapped.
- We need to ensure the
sender
is actually a UIButton
instance. Not critical, but if we don't at least ensure it's a UIView
, we won't be able to get the superview
.
- Once we have that, ensure the button is inside a
superview
, which we assume to be the contentView
.
- Check for the superview of the content view, and we'll have the cell itself.
- At this point we can ask the collection view (which we have a reference to, because of the outlet set up above) for the index path to the cell.
- Once we have an index path, we can update our label or do something with the data backing the cell.
Notes
- While testing this I see that you may not actually need to remove the target-action anymore - UIKit seems to do the right thing now and only calls your method once.
- The code we wrote here is available on GitHub.
Edit:
@DonMag pointed out that you're using a nib instead of a storyboard, which I overlooked initially. So, here's the process for getting it all set up with nibs:
- We can use the same cell class as above, but we need to add a separate nib for it, because the view controller nib doesn't support cell prototypes.
- We need to register the nib separately, for the same reason. Including a cell prototype in the storyboard does this automatically. When working with nibs, you don't get this for free.
- We need to set up the view controller in the nib.
- Almost all of the other code can be moved over from the storyboard version.
Let's start by making a new nib for the CustomCollectionViewCell
. Using the "Empty" template, make a new nib called "CustomCollectionViewCell.xib." The name doesn't have to match the class, but it's going to be used later on, so to follow convention, let's call it that.
In the new nib, drag out a collection view cell from the palette on the bottom right, and then add the label to it. Set the custom class to CustomViewControllerCell
(or whatever yours is called) and connect the label to the outlet.
Next, let's make a new View Controller. We probably can re-use the initial one, but so that we don't register two cells for the same identifier, let's stick to a new UIViewController
subclass. To get the nib, check "Also create XIB file."
Inside the new nib-based view controller, we want the same outlets as we had before. We also want the same UICollectionViewDataSource implementation. There's one thing that's different here: we need to register the cell.
To do so, let's add a method called registerCollectionViewCell()
which we'll call from viewDidLoad
. Here's what it looks like:
// MARK: - Setting Up The Collection View Cell
func registerCollectionViewCell()
{
guard let collectionView = self.collectionView else
{
print("We don't have a reference to the collection view.")
return
}
let nib = UINib(nibName: "CustomCollectionViewCell", bundle: Bundle.main)
collectionView.register(nib, forCellWithReuseIdentifier: "com.mosheberman.samples.cell")
}
Our viewDidLoad()
method looks like this now:
override func viewDidLoad() {
super.viewDidLoad()
self.registerCollectionViewCell()
// Do any additional setup after loading the view.
}
Inside the nib, lay out the label and collection view as we did before. Wire up the outlets and the collection view data source, and delegate, and we should be done!
A couple of other things:
- In my demo project, I needed to also present the nib-based version of the view controller. I did that in the app delegate by replacing whatever the app's main storyboard loaded in
application(_:didFinishLaunchingWithOptions:)
- Your storyboard nib should have the File's Owner left to the default value of
NSObject
. The cell should have a custom class matching whatever class you want to load.