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()
Related
I have a collection view where each of the cells has a delete button. I added the following code to cellForItemAt indexPath function.
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellTwo", for: indexPath) as! CustomCellTwo
cell.deleteButton.layer.setValue(indexPath.row, forKey: "index")
cell.deleteButton.addTarget(self, action: #selector(deleteCell), for: .touchUpInside)
Initially it looked as if it was working great. However, I found out that the add target function does not get called at the first tap if I scroll back and forth and then tap the delete button. If I tap again, it works as expected. Only the first tap does not work.
I have been trying to find a reason and a solution for several hours... Please help provide any ideas and advice.
Try to move buttons handling into CustomCellTwo implementation. Handle button event touchUpInside with #IBAction func. Now you can debug it with breakpoint set in this function's body.
Also add closure type variable to your CustomCellTwo to pass deleteCell calls into it. So it could also be checked with breakpoint.
Example:
class CustomCellTwo: UICollectionViewCell {
var onDelete: (() -> Void)?
#IBAction func onDeleteButtonTouch(_ sender: Any) {
onDelete?()
}
}
// in your UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellTwo", for: indexPath) as! CustomCellTwo
cell.onDelete = {
self.deleteCell(indexPath)
}
}
I have a collectionView showing cells. When I drag/touch/slide my finger on an item, if the touch ends on the item, the cell is selected (segues to the details screen).
Is there any way to limit cell selection (didSelectItemAt indexPath) to a simple tap? i.e it shouldn't select the cell if finger is dragged on an item and the touch ends on it.
Is this the default behavior?
I feel like it might be the cause of a cryptic issue with my custom navigation.
Thanks
Do add Following in your cellForItem
let tap = UITapGestureRecognizer(target: self, action: #selector(cellTapped(tapGestureRecognizer:)))
tap.numberOfTapsRequired = 1
cell.addGestureRecognizer(tap)
And add following function
#IBAction func cellTapped(tapGestureRecognizer: UITapGestureRecognizer)
{
//Do your required work.
}
You can use UITapGestureRecognizer, cause it will only respond on Tap gesture:
#objc func tapAction(_ sender: UITapGestureRecognizer) {
// TODO: - Action you need
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: <CellReuseId>, for: indexPath)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
cell.contentView.addGestureRecognizer(tap)
return cell
}
But in this way didSelectItemAt will not work.
I am Trying to add a Tap Gesture Recognizer to the header of my UICollection view, but no matter what, I can't get the numberOfPostsViewTapped() function to fire off. I've been trying for hours, and have tried using other UI elements such as other views or labels in the header view, but nothing is helping. Some guidance would be much appreciated.
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader: // only checking header - no footer on this view
// use an external class for the header UICollectionViewCell in order to set outlets on a non-reusable cell
// if you try to set outlets on a reusable cell, such as a header, it will fail
let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "Header", forIndexPath: indexPath) as! ProfileCollectionViewHeader
// dynamically set user profile information
headerView.usernameTextLabel.text = user?.name
headerView.numberOfPostsTextLabel.text = user?.numberOfPosts != nil ? "\(user!.numberOfPosts!)" : "0"
let numberOfPostsViewSelector : Selector = #selector(self.numberOfPostsViewTapped)
let viewPostsViewGesture = UITapGestureRecognizer(target: self, action: numberOfPostsViewSelector)
viewPostsViewGesture.numberOfTapsRequired = 1
viewPostsViewGesture.delaysTouchesBegan = true
headerView.numberOfPostsTextLabel.userInteractionEnabled = true;
headerView.numberOfPostsTextLabel.addGestureRecognizer(viewPostsViewGesture)
return headerView
default:
assert(false, "Unexpected element kind")
}
}
func numberOfPostsViewTapped(sender: UITapGestureRecognizer){
print("HErE")
}
Check your frames. Is your UICollectionView within its frame?
If a view is out of its frame, and doesn't clip to its bounds, it will show the view's contents, though user interaction will not work with it.
I think you need to set userInteractionEnabled to true to your headerView so that the tapping would reach your label which is a child of your headerView.
headerView.userInteractionEnabled = true
Made few changes to your code and this worked for me
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
guard
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "AddLinksReusableView", for: indexPath) as? AddLinksReusableView else {
fatalError("Invalid view type")
}
let numberOfPostsViewSelector : Selector = #selector(self.imgVuTapped)
let viewPostsViewGesture = UITapGestureRecognizer(target: self, action: numberOfPostsViewSelector)
headerView.profileImg.isUserInteractionEnabled = true
viewPostsViewGesture.numberOfTapsRequired = 1
viewPostsViewGesture.delaysTouchesBegan = true
headerView.profileImg.addGestureRecognizer(viewPostsViewGesture)
return headerView
default:
assert(false, "Invalid element type")
}
}
#objc func imgVuTapped (sender: UITapGestureRecognizer){
print("HErE, Its working")
}
Happy Codding
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
I want to add a tap event to my CollectionViewCell and to pass there my cell with the data it has. How can I achieve this?
Should this event be handled by my ViewController or by CollectionViewCell?
My ViewController:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
cell.imgImage.image = imageArray[indexPath.row]
cell.url = "xhini"
return cell
}
My CollectionViewCell:
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var imgImage: UIImageView!
var url: String = "url"
}
Implement UICollectionViewDelegate and then you can use following method in the ViewController to react to selecting a cell:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let image = imageArray[indexPath.row]
// do stuff with image, or with other data that you need
}
Don't forget to set the delegate where you set a data source:
collectionView.dataSource = self
// add this line:
collectionView.delegate = self
UPDATE
Or if you are using a storyboards, you want to set it using storyboards the same way as you set a dataSource for the dataSource of the tableView:
UPDATE 2
Your tap gesture recognizer cancels event for the collection view, so to deal with this, just uncomment the line tap.cancelsTouchesInView = false, and it will work:
//Looks for single or multiple taps.
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
//Uncomment the line below if you want the tap not not interfere and cancel other interactions.
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
I saw your code which you shared in the above answer by #Milan and figured out the reason.
You have added a tap gesture on viewDidLoad of ViewController :
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
This makes the UICollectionView's didSelectItemAt not getting called.
So comment this code and it should work.
For this gesture, you have to find another approach