Get button click from reusable cell in collection

2019-06-10 01:30发布

I have created reusable cell with include a button in a XIB for a collection.

I can change the text of labels and buttons in the collection view but i can't get on click event.

I have tried below options:

a. This in UICollectionViewCell : Not working

class cellVC: UICollectionViewCell {
    @IBOutlet weak var sampleLabel: UILabel!
    @IBOutlet weak var buttonClicked: UIButton!

    @IBAction func buttonTest(_ sender: Any) {
        print("dsfsdf   111111")
    }
}

b. I have also tried : Not working

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{

    let cell : cellVC = collectionView.dequeueReusableCell(withReuseIdentifier: "cellVC", for: indexPath) as! cellVC
    cell.sampleLabel. = "sample text"
    cell.buttonClicked.tag = indexPath.row
    cell.buttonClicked.addTarget(self, action: #selector(masterAction(sender:)), for: .touchUpInside)

    cell.buttonClicked.backgroundColor = UIColor.cyan
    return cell
}


func masterAction(_ sender: UIButton) {
    print ("button click")
}

As both solutions are not working, how do i achieve this.

Thanks

标签: ios swift swift3
5条回答
Ridiculous、
2楼-- · 2019-06-10 01:49

thanks @Thomas If anyone wants the solution, the answer up top has been updated in swift :

self.contentView.isUserInteractionEnabled = false
查看更多
我只想做你的唯一
3楼-- · 2019-06-10 01:50

instead of

 cell.buttonClicked.addTarget(self, action: #selector(masterAction(sender:)), for: .touchUpInside)

write

 cell.buttonClicked.addTarget(self, action: #selector(masterAction(_:)), for: .touchUpInside)
查看更多
霸刀☆藐视天下
4楼-- · 2019-06-10 01:59

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: An image of a collection view cell with a single centered button.

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.

An image of the Xcode inspector, showing the button wired up to its outlet

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:

A screenshot of Xcode inspector, showing a collection view cell's identifier

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:

  1. A label to show that our button inside the cell was tapped
  2. 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.)
  3. Some code to show our custom cell.
  4. 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.

  1. 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.
  2. Once we have that, ensure the button is inside a superview, which we assume to be the contentView.
  3. Check for the superview of the content view, and we'll have the cell itself.
  4. 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.
  5. 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:

  1. 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.
  2. 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.
  3. We need to set up the view controller in the nib.
  4. 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.
查看更多
成全新的幸福
5楼-- · 2019-06-10 02:00

You can go with:

    class cellVC: UICollectionViewCell {
        @IBOutlet weak var sampleLabel: UILabel!
        @IBOutlet weak var buttonClicked: UIButton!

        var buttonTapAction: ((cellVC) -> ())?

        @IBAction func buttonTest(_ sender: Any) {
            buttonTapAction?(self)
        }
    }
    
    //////////
    
    
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{

    let cell : cellVC = collectionView.dequeueReusableCell(withReuseIdentifier: "cellVC", for: indexPath) as! cellVC
    cell.sampleLabel. = "sample text"
    cell.buttonClicked.tag = indexPath.row
  
    cell.buttonTapAction = { cell in 
    
    //Tap handling logic
    
    }

    cell.buttonClicked.backgroundColor = UIColor.cyan
    return cell
}

查看更多
做个烂人
6楼-- · 2019-06-10 02:06

This sometimes happen when the button is linked to the File's Owner instead of your cell.

Right click on the button in IB and check if the referencing outlet is correct, this should be your cell and not the File's Owner like in the image below:

enter image description here

查看更多
登录 后发表回答