This question already has answers here:
UIButton causing unrecognized selector sent to instance
(5 answers)
Closed 5 years ago.
I am using a custom cell with UICollectionView, I need to define UIButton programmatically per cell.
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! ClinicListCell
cell.title.text = clinicNames[indexPath.row]
cell.subTitle.text = clinicSubs[indexPath.row]
cell.backgroundImageView.image = UIImage(named: clinicImages[indexPath.row])
cell.profileBtn.tag = indexPath.row
cell.profileBtn.addTarget(self, action: Selector(("profileBtnClicked:")), for: .touchUpInside)
return cell
}
And I have defined the following selector method in the same class.
class func profileBtnClicked(sender:UIButton) {
print("Selected")
}
I've tried by removing class/static from the selector method, but it always give me unrecognized selector sent to instance error, where am I going wrong?
Thanks.
Try this
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! ClinicListCell
cell.title.text = clinicNames[indexPath.row]
cell.subTitle.text = clinicSubs[indexPath.row]
cell.backgroundImageView.image = UIImage(named: clinicImages[indexPath.row])
cell.profileBtn.tag = indexPath.row
cell.profileBtn.addTarget(self, action: #selector(YourViewController.profileBtnClicked(sender:)), for: UIControlEvents.touchUpInside)
return cell
}
class func profileBtnClicked(sender:UIButton) {
print("Selected")
}
Is the profileBtn linked up probably? This error may happens when the name has changed or the link between the button in interface builder and the variable is removed.
Or you can try with the syntax
cell.profileBtn.addTarget(self, action: #selector("profileBtnClicked:"), for: .touchUpInside)
Try adding a #objc to your method, not strictly required in this case, and remove the class or static qualifier.
By the way, since Swift 2.2, you can create a Selector from a Swift function using the #selector operator. For instance:
let clicked = #selector(self.profileBtnClicked(sender:))
for:
#objc func profileBtnClicked(sender: UIButton) {
...
}
Technically, for NSObject based classes, the #obj qualifier should only be required for private methods.
Related
I have a collection view and image view inside it and I added a UIButton to delete the image after selection. When I click the button it crashes and gives me this error:
AdPostViewController deleteUser]: unrecognized selector sent to instance 0x7fb588d5b7f0
Why is this happening and how do I fix this? Here is my code:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as! ImageCell
let img = self.PhotoArray[indexPath.row]
cell.image.image = img
cell.deleteButton?.layer.setValue(indexPath.row, forKey: "index")
cell.deleteButton?.addTarget(self, action: Selector(("deleteUser")), for: UIControl.Event.touchUpInside)
return cell
}
func deleteUser(_ sender: UIButton) {
let i: Int = (sender.layer.value(forKey: "index")) as! Int
PhotoArray.remove(at: i)
// PhotoArray.removeAtIndex(i)
ImagesCollectionView.reloadData()
}
One problem is that you are forcing manual formation of the Objective-C selector, and you don't actually know how to form an Objective-C selector manually so you are getting it wrong. Don't do that! Let the compiler form the selector for you. That's its job. Replace
action: Selector(("deleteUser"))
with
action: #selector(deleteUser)
Also, you need to expose your deleteUser method to Objective-C explicitly:
#objc func deleteUser(_ sender: UIButton) {
Otherwise Objective-C still won't be able to introspect your class and find this method when the time comes to call it. Fortunately, when you switch to #selector syntax, the compiler will call out that issue for you!
I'm trying to allow user interaction in my collection view. I have decided to try to implement UITapGestureRecognizer to do this. I have tried adding a UITapGestureRecognizer to the collectionview itself and to the collectionview cell. Both ways crash the app. Here is how I am adding the UITapGestureRecognizer to the cell.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionview.dequeueReusableCell(withReuseIdentifier: "userCell", for: indexPath) as! UserCell
cell.userImage.sd_setImage(with: URL(string: self.user[indexPath.row].imagePath))
cell.nameLabel.text = self.user[indexPath.row].username
cell.userID = self.user[indexPath.row].userID
let singleTap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "segueToProfile:")
singleTap.numberOfTapsRequired = 1
singleTap.numberOfTouchesRequired = 1
cell.addGestureRecognizer(singleTap)
return cell
}
When I tap on the cell I get a SIGABRT in the AppDelegate. The error message reads "terminating with uncaught exception of type NSException". What am I doing wrong. UITapGestureRecognizer.
This is my segueToProfile function:
func segueToProfile(gesture: UITapGestureRecognizer) {
// if(recognizer.state == UIGestureRecognizer.State.ended){
// print("myUIImageView has been tapped by the user.")
// }
print("hell world")
}
If you use didSelectItemAt method of collectionView your codebase look readable and maintainable.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
currentViewController.performSegue(withIdentifier: "YourSegueName", sender: nil)
}
First, I would get rid of tapgesturerecognizer as didSelectItem should handle what you are trying to accomplish. That being said, in order for this to work you must:
Remove tapgesturerecognizer
Ensure that the collection view delegate is set to self.
e.g. <yourColletionViewName>.delegate = self
Above can be assigned at viewDidLoad()
i don't know what happen, i set the button.tag with the table row and when it reach row > 1, it will throw lldb. it works if the button.tag <= 1
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cells")! as UITableViewCell
let alertBtn = cell.viewWithTag(1) as! UIButton;
alertBtn.tag = indexPath.row
alertBtn.addTarget(self, action: Selector(("showAlert:")), for: UIControlEvents.touchUpInside)
return cell
}
Application crash on this line, because it fails to find a view with tag 1, the tag is updating in every cell with row value.
let alertBtn = cell.viewWithTag(1) as! UIButton
remove this line and Take #IBOutlet for alertBtn From UITableViewCell instead of refreshing with tag.
Swift 3X...
You are replacing your tag so first tag items are getting nil so replace this code ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cells")! as UITableViewCell
let alertBtn = cell.viewWithTag(1) as! UIButton
alertBtn.addTarget(self, action: #selcetor(showAlert(sender:))), for: .touchUpInside)
return cell
}
func showAlert(sender:UIButton) {
let point = sender.convert(CGPoint.zero, to: self.tableview)
let indexpath = self.tableview.indexPathForRow(at: point)
}
Try to do custom UITableViewCell.
Declare protocol and delegate for Your new class class. Wire up a action and call delegate
protocol MyCellDelegate: class {
func buttonPressed(for cell: MyCell)
}
class MyCell:UITableViewCell {
weak var delegate: MyCellDelegate?
#IBAction func buttonPressed(sender: Any){
self.delegate?.buttonPressed(for: self)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
.......
cell.delegate = self
........
}
Remember to add new protocol implementation to Your VC. You can add prepareForReuse method and reset delegate to nil when cell is reused.
If you want to get indexPath of cell containing tapped button you can use function similar to this matching your requirement.
func showAlert(sender: AnyObject) {
if let cell = sender.superview?.superview as? UITableViewCell{ // do check your viewchierarchy in your case
let indexPath = itemTable.indexPath(for: cell)
}
print(indexPath)// you can use this indexpath to get index of tapped button
}
Remove this line from cellForRowAtIndexPath alertBtn.tag = indexPath.row
If you can use Custom Cell for this purpose you can get indexpath of selected button as you were getting previously.
Create CustomCell and create IBOutlet for your button and labels etc. You can access subviews of your cell in cellForRowAtIndexPath and assign tag to your button. If you have any queries regarding CustomCell do let me know.
I have a custom button in my collection view cell. I just want to pass indexPath to it but I am getting
"unrecognized selector error"
Here is my code
cell.showMapButton.addTarget(self, action: #selector(testFunc(indexPath:)), for: .touchUpInside)
and the function is
func testFunc(indexPath: IndexPath){
print("Testing indexPath \(indexPath)")
}
If I remove the indexPath argument it works fine and the function gets called but I need that argument so please help me in resolving this issue.
In the addTarget(:action:for:) method for UIButton, the action can at most accept a single UIButton or any of it's superclass as parameter. If you need the indexPath of your button, you need to make it a property of your UIButton by subclass or other means. My way of doing it is to create a subclass of UIButton that have indexPath as it's property:
class ButtonWithIndexPath: UIButton {
var indexPath:IndexPath?
}
Then add target as normal:
cell.showMapButton.addTarget(self, action: #selector(testFunc(button:)), for: .touchUpInside)
Not forgetting to set the indexPath of your button to that of which ever cell it is in
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath) as! myCell
cell.button.indexPath = indexPath
...
return cell
}
And cast it into it's custom subclass in the function to read the indexPath:
func textFunc(button: UIButton) {
let currentButton = (button as! ButtonWithIndexPath)
print(currentButton.indexPath)
}
You can pass UIButton instance with target selector parameters for button action.
try with following code:
Add/replace below code, belongs to collection view cell into your collection view data source method - cellForRowAtIndexPath
cell.showMapButton.tag = indexPath.row
cell.showMapButton.addTarget(self, action: #selector(testFunc(button:)), for: .touchUpInside)
For Swift 4 - define your selector function using #objc, like below.
#objc func testFunc(button: UIBUtton){
print("Index = \(button.tag)")
}
Here's a problem which I have been stuck at for quite some time now.
Here's the code
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({() -> Void in
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
self.collectionViewLove?.reloadData()}, completion: nil)}
I have a button inside each UICollectionViewCell which deletes it on clicking. The only way for me to retrieve the indexPath is through the button tag. I have initialized the button tag in
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
However every time I delete, the first time it deletes the corresponding cell whereas the next time it deletes the cell follwing the one I clicked. The reason is that my button tag is not getting updated when I call the function reloadData().
Ideally, when I call the reloadData() ,
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
should get called and update the button tag for each cell. But that is not happening. Solution anyone?
EDIT:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: UIImage(named: "preloader"))
let item = self.wishlist?.results[indexPath.row]
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.text = item?.title
cell.itemName.numberOfLines = 1
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.price.adjustsFontSizeToFitWidth = true
cell.deleteButton.tag = indexPath.row
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = indexPath.row
cell.buyButton.backgroundColor = UIColor.blackColor()
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
A couple of things:
You're doing too much work in cellForItemAtIndexPath--you really want that to be as speedy as possible. For example, you only need to register the nib once for the collectionView--viewDidLoad() is a good place for that. Also, you should set initial state of the cell in the cell's prepareForReuse() method, and then only use cellForItemAtIndexPath to update with the custom state from the item.
You shouldn't reload the data until the deletion is complete. Move reloadData into your completion block so the delete method is complete and the view has had time to update its indexes.
However, it would be better if you didn't have to call reloadData in the first place. Your implementation ties the button's tag to an indexPath, but these mutate at different times. What about tying the button's tag to, say, the wishlist item ID. Then you can look up the appropriate indexPath based on the ID.
Revised code would look something like this (untested and not syntax-checked):
// In LoveListCollectionViewCell
override func prepareForReuse() {
// You could also set these in the cell's initializer if they're not going to change
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.numberOfLines = 1
cell.price.adjustsFontSizeToFitWidth = true
cell.buyButton.backgroundColor = UIColor.blackColor()
}
// In your UICollectionView class
// Cache placeholder image since it doesn't change
private let placeholderImage = UIImage(named: "preloader")
override func viewDidLoad() {
super.viewDidLoad()
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: placeholderImage)
let item = self.wishlist?.results[indexPath.row]
cell.itemName.text = item?.title
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.deleteButton.tag = item?.id
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = item?.id
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
func removeFromLoveList(sender: AnyObject?) {
let id = sender.tag
let index = wishlist?.results.indexOf { $0.id == id }
let indexPath = NSIndexPath(forRow: index, inSection: 0)
collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
wishlist?.results.removeAtIndex(index)
}
It's probably not a good idea to be storing data in the cell unless it is needed to display the cell. Instead your could rely on the UICollectionView to give you the correct indexPath then use that for the deleting from your data source and updating the collectionview.
To do this use a delegate pattern with cells.
1.Define a protocol that your controller/datasource should conform to.
protocol DeleteButtonProtocol {
func deleteButtonTappedFromCell(cell: UICollectionViewCell) -> Void
}
2.Add a delegate property to your custom cell which would call back to the controller on the delete action. The important thing is to pass the cell in to that call as self.
class CustomCell: UICollectionViewCell {
var deleteButtonDelegate: DeleteButtonProtocol!
// Other cell configuration
func buttonTapped(sender: UIButton){
self.deleteButtonDelegate.deleteButtonTappedFromCell(self)
}
}
3.Then back in the controller implement the protocol function to handle the delete action. Here you could get the indexPath for the item from the collectionView which could be used to delete the data and remove the cell from the collectionView.
class CollectionViewController: UICollectionViewController, DeleteButtonProtocol {
// Other CollectionView Stuff
func deleteButtonTappedFromCell(cell: UICollectionViewCell) {
let deleteIndexPath = self.collectionView!.indexPathForCell(cell)!
self.wishList.removeAtIndex(deleteIndexPath.row)
self.collectionView?.performBatchUpdates({ () -> Void in
self.collectionView?.deleteItemsAtIndexPaths([deleteIndexPath])
}, completion: nil)
}
}
4.Make sure you set the delegate for the cell when configuring it so the delegate calls back to somewhere.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//Other cell configuring here
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("identifier", forIndexPath: indexPath)
(cell as! CustomCell).deleteButtonDelegate = self
return cell
}
}
I was facing the similar issue and I found the answer by just reloading collection view in the completion block.
Just update your code like.
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
}, completion: {
self.collectionViewLove?.reloadData()
})
which is mentioned in UICollectionView Performing Updates using performBatchUpdates by Nik