Hello on a specific viewController of my project i have a UICollectionView with a custom class cell. But i have a big problem this that func :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("tapped on a cell")
}
when is click on a cell and instant realease (normal click) it does nothing, just nothing.
if i press and hold about 1s without release the finger it become grey, highlighted
And if i press and hold at least 3 seconds release the finger didSelectItemAt is executed correctly.
I tried to do the same on another project and that's work great but not on this VC and i really don't find the problem. The VC Bugged is of addTest clas in Main.storyboard
The insight of Mojtaba Hosseini is very clever, but the answer given might not be quite correct.
It turns out that there is a UITapGestureRecognizer on the main view; if it recognizes before the tap on the cell, it prevents cell selection. But if you merely set cancelsTouchesInView to false on that gesture recognizer, then they both operate, and that seems unlikely to be what is wanted. We surely want the cell tap and not the tap gesture recognizer tap.
The correct solution is thus to give the tap gesture recognizer a delegate and implement gestureRecognizerShouldBegin. Here, we look to see where the tap is. If it is within the bounds of a cell, we return false; otherwise we return true. We thus mediate between the cell tap and gesture recognizer tap.
Here is a possible implementation, demonstrated in a highly simplified form:
extension UIView {
func isDescendant(of whattype:UIView.Type) -> Bool {
var sup : UIView? = self.superview
while sup != nil {
if (whattype == type(of:sup!)) {
return true
}
sup = sup!.superview
}
return false
}
}
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let t = UITapGestureRecognizer(target: self, action: #selector(tap))
self.view.addGestureRecognizer(t)
t.delegate = self
}
#objc func tap(_:UIGestureRecognizer) {
print("tap")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("select")
}
func gestureRecognizerShouldBegin(_ gr: UIGestureRecognizer) -> Bool {
if let v = gr.view {
let loc = gr.location(in: v)
if let v2 = v.hitTest(loc, with: nil) {
return !v2.isDescendant(of: UICollectionViewCell.self)
}
}
return true
}
}
As you can see, we look to see whether the tap is inside a collection view cell; if it is, our gesture recognizer is prevented from recognizing, and the selection succeeds immediately.
Probably there is a UIGesture or another interactable thing underneath the collection view. You should DISABLE its ability to cancel touches in view in interface builder:
or in code:
myTapGestureRecognizer.cancelsTouchesInView = false
Related
So i have a horizontal UICollectionView of images that is inside a vertical UICollectionView and i want to detect which cell is in the center on the horizonal UICollectionView when i select a cell from the vertical one
I tried to send a notification and call a function that does the work but its called multiple times since its reusing the same cell so at the end i dont get the appropriate indexPath.
And also when i tap on an image the "didSelectItemAt" of the horizontal collectionView is called, is there a way to get the vertical one to be called instead ?
thanks
Your best bet would be delegates via protocols
Create protocol for your collection view cells
protocol yourDelegate: class {
func didSelectCell(WithIndecPath indexPath: IndexPath, InCollectionView collectionView: UICollectionView) -> Void
}
In your cells, create a function called setup which you can call at cellForRow.
In your cells, create a touch recogniser for self.
Disable cell selection for your collection views since these delegates will be called when the user touches given cell.
class yourCell: UICollectionViewCell {
var indexPath: IndexPath? = nil
var collectionView: UICollectionView? = nil
weak var delegate: yourDelegate? = nil
override func awakeFromNib() {
super.awakeFromNib()
let selfTGR = UITapGestureRecognizer(target: self, action: #selector(self.didTouchSelf))
self.contentView.addGestureRecognizer(selfTGR)
}
#objc func didTouchSelf() {
guard let collectionView = self.collectionView, let indexPath = self.indexPath else {
return
}
delegate?.didSelectCell(WithIndecPath: indexPath, InCollectionView: collectionView)
}
func setupCell(WithIndexPath indexPath: IndexPath, CollectionView collectionView: UICollectionView, Delegate delegate: yourDelegate) {
self.indexPath = indexPath
self.collectionView = collectionView
self.delegate = delegate
}
}
In your viewController, create extension for this protocol and if you do everything right, when the user touches your cell, the cell will call you via this delegation.
extension YourViewController: yourDelegate {
func didSelectCell(WithIndecPath indexPath: IndexPath, InCollectionView collectionView: UICollectionView) {
//You have your index path and you can "if" your colllection view
if collectionView == self.yourFirstCollectionView {
} else if collectionView == self.yourSecondCollectionView {
}
//and so on..
}
}
since protocol is ": class", we can use weak for your delegate property so no memory leak will occur. I use this method for tableViews and collectionViews throughout my projects.
Currently, I have a collectionView and its only function is to let the user choose a color. When the user chooses a color (clicks a cell), a selected indicator (UIView) animates inside the cell, letting the user know that that certain color is selected. The core functionality is there, but my question is based more off of UI. When I scroll, the cell thinks I am clicking it and it sets isSelected to true for a second until I remove my finger from the cell. This causes an animation to happen at unwanted times. So, I think the problem is in shouldSelectItemAt in the CollectionViewDelegate methods. With UIGestureRecognizer I can measure whether or not the sender state has .began or .ended. Would my question call for this or something similar?
Here are the selection collectionView delegate methods.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = colorPreviewCollectionView.cellForItem(at: indexPath) as! TrayColorPreviewCell
// stuff with cell happens here
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
let cell = colorPreviewCollectionView.cellForItem(at: indexPath) as! TrayColorPreviewCell
guard cell.isSelected else { return true }
return false
}
In the TrayColorPreviewCell (custom UICollectionViewCell) class, I use the isSelected to run the animation. This animation is the one that is happening repeatedly when scrolling.
override var isSelected: Bool {
didSet {
changeSelectionStatus()
}
}
func changeSelectionStatus() {
if isSelected {
// animation to indicate selection
} else {
// animation to indicate changed selection
}
}
I have an ImageView inside a CollectionViewCell. I want to be able to click the image and it take me to another ViewController. How would I do this? This is the code I have so far.
import UIKit
class FirstViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
var images = ["meal1", "photograph1", "meal1"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.collectionView.delegate = self
self.collectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionCell", for: indexPath) as! CollectionViewCell
//set images
cell.imageView.image = UIImage(named: images[indexPath.row])
return cell
}
}
As you said you want to detect image tap on collectionview cell please go through this code :
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.connected(_:)))
cell.yourImageView.isUserInteractionEnabled = true
cell.yourImageView.tag = indexPath.row
cell.yourImageView.addGestureRecognizer(tapGestureRecognizer)
And add below method to your ViewController
func connected(_ sender:AnyObject){
print("you tap image number : \(sender.view.tag)")
//Your code for navigate to another viewcontroller
}
Note - Make sure your user interection for cell image is enable
Add a tabGestureRecognizer to your imageview in collectionView "cellForItemAt" method, and in the method of recognizer tap call the segue to go to the desired viewcontroller.
Swift 5
CollectionView Function:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.tap(_:)))
cell.yourImg.isUserInteractionEnabled = true
cell.yourImg.tag = indexPath.row
cell.yourImg.addGestureRecognizer(tapGestureRecognizer)
return cell
}
Tap Function:
#IBAction func tap(_ sender:AnyObject){
print("ViewController tap() Clicked Item: \(sender.view.tag)")
}
You can either us a UIButton and set the image property on it with no title, or you can add a UIGestureRecognizer to the UIImageView. Either way, you'd just Present or Show the ViewController you want to display once the action has been received.
One thing I'll often do in this situation, is create a CollectionCellDelegate protocol that has a callback function (something like buttonPressed:forCollectionCell:), that I can have my CollectionView conform to, then set the delegate of each cell to the CollectionView. Then you can call up to the CollectionView when the button/image is pressed, and have the CollectionView handle whatever behaviour you want, in this case, presenting/pushing a new view controller.
I'm using collection view and want to move cells when user paned. This is the code.
func longPressPhotoCollectionView(sender: UILongPressGestureRecognizer) {
guard isEditing else {
return
}
let point = sender.location(in: photoCollectionView)
switch sender.state {
case .began:
guard let indexPath = photoCollectionView.indexPathForItem(at: point) else {
return
}
let isS = photoCollectionView.beginInteractiveMovementForItem(at: indexPath)
print(isS)
case .changed:
photoCollectionView.updateInteractiveMovementTargetPosition(point)
case .ended:
photoCollectionView.endInteractiveMovement()
default:
photoCollectionView.cancelInteractiveMovement()
}
}
I doubt custom layout will cause this problem. And I found this from Apple's Doc.
When you call this method, the collection view consults its delegate to make sure the item can be moved. If the data source does not support the movement of the item, this method returns false.
I don't know how to handle animations, especially with custom layout. Please help me! Thanks!
Implement the following in your UICollectionViewDataSource:
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
print("MOVING!")
// Switch the Items in the DataSource
}
When you call this method, the collection view consults its delegate to make sure the item can be moved. If the data source does not support the movement of the item, this method returns false.
The Apple documentation is misleading. It seems to imply that you should override some method on UICollectionViewDelegate to make a cell movable. In fact, it's a UICollectionViewDataSource method:
optional func collectionView(_ collectionView: UICollectionView,
canMoveItemAt indexPath: IndexPath) -> Bool
https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618015-collectionview
I want to make an infinite scroll collection view, so in this code I add 10 to the number of cells every time the index path is equals to the last cell item, and then reload the data in the collection. The function works; I can scroll infinitely, but if I stop, and then scroll a little up or down, everything is blank. The cells aren't showing.
My theory is that it has something to do with dequeueReusableCellWithReuseIdentifier, and it decides to only show the cells that are currently on the screen.
View when scrolling (I added the numbers to the cells outside of Xcode)
View with the missing cells above when scrolling a bit after stopping
private let reuseIdentifier = "Cell"
private var numberOfCells = 20
class CollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)
if indexPath.row == numberOfCells - 1 {
numberOfCells += 10
self.collectionView?.reloadData()
}
cell.contentView.backgroundColor = UIColor.blueColor()
return cell
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return numberOfCells
}
}
Calling reloadData from cellForItemAtIndexPath is bad. Use other delegate method for that, for example, scrollViewDidScroll.
var numberOfCells: Int = 20 {
didSet {
if numberOfCells != oldValue {
self.collectionView?.reloadData()
}
}
}
I am doing similar things in Table View and it is working for me. The only difference is that if indexPath.row is last cell then I am calling another function which do some stuff (required for me) and calling reloadData() into that function only. Try this way, I am not sure, but it may solve your problem.
Swift 4.2
reload it like this.
DispatchQueue.main.async(execute: collectionView.reloadData)