I have a collection view where when each cell is tapped a larger version of the cell image pops up and disappears when tapped again. On top of this I'd like to be able to select a view in the corner of the cell that displays a blue checkmark(SSCheckMark View) or a greyed out checkmark when tapped again. My current code is:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as! PhotoCell
cell.backgroundColor = .clear
cell.imageView.image = UIImage(contentsOfFile: imagesURLArray[indexPath.row].path)
cell.checkmarkView.checkMarkStyle = .GrayedOut
cell.checkmarkView.tag = indexPath.row
cell.checkmarkView.checked = false
let tap = UITapGestureRecognizer(target: self, action: #selector(checkmarkWasTapped(_ :)))
cell.checkmarkView.addGestureRecognizer(tap)
return cell
}
func checkmarkWasTapped(_ sender: SSCheckMark) {
let indexPath = IndexPath(row: sender.tag, section: 1)
if sender.checked == true {
sender.checked = false
} else {
sender.checked = true
}
collectionView.reloadItems(at: [indexPath])
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
addZoomedImage(indexPath.row)
addGestureToImage()
addBackGroundView()
view.addSubview(selectedImage)
}
But when I run and select the checkmark view I get the error:
unrecognized selector sent to instance on the first line of checkmarkWasTapped() i can see that it doesn't like sender but i don't know why. Any help would be great.
UITapGestureRecognizer tap's sender is the gesture. The checkmarkWasTapped method definition is wrong. And you can get the checkmarView using sender.view. Try this.
func checkmarkWasTapped(_ sender: UIGestureRecognizer) {
let checkmarkView= sender.view as? SSCheckMark
let indexPath = IndexPath(row: checkmarkView.tag, section: 1)
if checkmarkView.checked == true {
checkmarkView.checked = false
} else {
checkmarkView.checked = true
}
collectionView.reloadItems(at: [indexPath])
}
Related
Right now, I have a bunch of messages in a collectionview cell.
My code to single tap the cell right now is
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Which cell: ", indexPath)
}
How do I make it such that this will only print if it is double tapped not single tapped?
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "customCell", for: indexPath) as! customCell
let tapCell = UITapGestureRecognizer(target: self, action: #selector(self.doubleTap(selectedIndex:)))
tapCell.numberOfTapsRequired=2
cell.tag=indexPath.row
cell.addGestureRecognizer(tapCell)
return cell
}
#objc func doubleTap(gesture: UITapGestureRecognizer){
print("Selected Index Is", gesture.view?.tag)
}
You can add UITapGestureRecognizer in collection view.
private var doubleTapGesture: UITapGestureRecognizer!
func setUpDoubleTap() {
doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTapCollectionView))
doubleTapGesture.numberOfTapsRequired = 2
collectionView.addGestureRecognizer(doubleTapGesture)
doubleTapGesture.delaysTouchesBegan = true
}
Call above method from your viewDidLoad as
override func viewDidLoad() {
super.viewDidLoad()
setUpDoubleTap()
}
Then add Gesture selector method in your class
#objc func didDoubleTapCollectionView() {
let pointInCollectionView = doubleTapGesture.location(in: collectionView)
if let selectedIndexPath = collectionView.indexPathForItem(at: pointInCollectionView) {
let selectedCell = collectionView.cellForItem(at: selectedIndexPath)
// Print double tapped cell's path
print("Which cell: ", selectedIndexPath.row)
print(" double tapped")
}
}
didDoubleTapCollectionView method will call only when you will double tap on collection view cell item.
I hope above example will solve your problem.
I have UICollectionView 2 rows 10+ cells.
deselected by default. when I click it becomes selected but when I click again not deselect.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath)
let cell = collectionView.cellForItem(at: indexPath)
let collectionActive: UIImageView = {
let image=UIImageView(image: #imageLiteral(resourceName: "collectionActive"))
image.contentMode = .scaleAspectFill
return image
}()
let collectionInactive: UIImageView = {
let image=UIImageView(image: #imageLiteral(resourceName: "collectionInactive"))
image.contentMode = .scaleAspectFill
return image
}()
if cell?.isSelected == true {
cell?.backgroundView = collectionActive
}else{
cell?.backgroundView = collectionInactive
}
}
how fix that problem?
in viewDidLoad()
collectionView.allowsMultipleSelection = true;
afterword I implemented these methods
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! MyCell
cell.toggleSelected()
}
func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! MyCell
cell.toggleSelected()
}
finally in my class
class MyCell : UICollectionViewCell {
func toggleSelected ()
{
if (selected){
backgroundColor = UIColor.redColor()
}else {
backgroundColor = UIColor.whiteColor()
}
}
}
For Swift 5 +
in viewDidLoad()
collectionView.allowsMultipleSelection = true
afterword I implemented these methods
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! MovieDetailsDateCollectionViewCell
cell.toggleSelected()
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! MovieDetailsDateCollectionViewCell
cell.toggleSelected()
}
In TableView Cell class
class MyCell : UICollectionViewCell {
func toggleSelected ()
{
if (isSelected){
backgroundColor = .red
}else {
backgroundColor = .white
}
}
}
If you don't want to enable multiple selection and only want one cell to be selected at a time, you can use the following delegate instead:
If the cell is selected then this deselects all cells, otherwise if the cell is not selected, it selects it as normal.
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
let cell = collectionView.cellForItem(at: indexPath) as! CustomCell
if cell.isSelected {
collectionView.selectItem(at: nil, animated: true, scrollPosition: [])
return false
}
return true
}
According to UICollectionView class doc, you can use:
var selectedBackgroundView: UIView? { get set }
You can use this view to give the cell a custom appearance when it is selected. When the cell is selected, this view is layered above the backgroundView and behind the contentView.
In your example in the cellForItem(at indexPath: IndexPath) -> UICollectionViewCell? function you can set:
cell.backgroundView = collectionInactive
cell.selectedBackgroundView = collectionActive
If the cell is selected, just set cell.isSelected = false in shouldSelectItemAt delegate and in a DispatchQueue.main.async { } block. So the state is actually changed to false (very) soon after the shouldSelectItemAt has been executed.
It may look like a hack but it actually works.
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
if let cell = collectionView.cellForItem(at: indexPath), cell.isSelected {
DispatchQueue.main.async { // change the isSelected state on next tick of the ui thread clock
cell.isSelected = false
self.collectionView(collectionView, didDeselectItemAt: indexPath)
}
return false
}
return true
}
Please let me know if you find/know any cons to do this. Thanks 🙏
In iOS 14 and newer, you can set the backgroundConfiguration property of a cell. Once set, all necessary visual effects for selecting and deselecting works automatically. You can use one of the preconfigured configurations, like this:
cell.backgroundConfiguration = .listSidebarCell()
…or create a UIBackgroundConfiguration object from scratch. You can also change a preconfigured configuration before applying.
More info here: https://developer.apple.com/documentation/uikit/uibackgroundconfiguration
I am trying to change some attributes of the custom view cell in my collection view outside of the cell, but I can not figure it out what I am doing wrong:
In cellForItem I have :
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PatientProfileCell", for: indexPath) as! PatientQuestionCell
if Auth.auth().currentUser?.uid == userID {
cell.commentButton.setTitle("Edit post", for: .normal)
cell.commentButton.addTarget(self, action: #selector(editPostAction), for: .touchUpInside)
}
} else {
print("Not the current user for collection view")
}
}
Everything works perfectly here but when I do trigger the action nothing happens. This is the function :
//post actions
func editPostAction() {
print("Edit post Enabled")
let cell = PatientQuestionCell()
cell.questionView.isEditable = true
cell.questionView.isSelectable = true
cell.questionView.isScrollEnabled = true
cell.questionView.becomeFirstResponder()
}
How can I change the attributes of the cell with this function?
You can make use of the tag property of UIButton like this:
Inside your collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) method, add this line of code:
cell.commentButton.tag = (indexPath.section * 100) + indexPath.item
Then set the target of your button like this:
cell.commentButton.addTarget(self, action: #selector(editPostAction(sender:)), for: .touchUpInside)
Now, add a new method, which is your selector:
func editPostAction(sender: UIButton) {
let section = sender.tag / 100
let item = sender.tag % 100
let indexPath = IndexPath(item: item, section: section)
let cell = self.collectionView?.cellForItem(at: indexPath) as! PatientQuestionCell
cell.questionView.isEditable = true
cell.questionView.isSelectable = true
cell.questionView.isScrollEnabled = true
cell.questionView.becomeFirstResponder()
}
I have two CollectionView's. One CollectionView (allHobbiesCV) is pre-populated with Hobbies you can select. The other CollectionView (myHobbiesCV) is empty, but if you tap on a hobby in the allHobbiesCV, it gets added to the myHobbiesCV. This is all working great.
I would like the tapped allHobbiesCV cells to switch to selected, it adds the hobby to myHobbiesCV, then if the user taps the same selected cell again in the allHobbiesCV, it removes that hobby from the myHobbiesCV. Basically a toggle add/remove.
Two things to note:
Users can manually select hobbies in myHobbiesCV, then tap a [Remove Hobby] button.
Hobbies will be sorted by seasons, so there will be 4 different data sets (Winter, Spring, Summer, Autumn) for allHobbiesArray. Depending on which season (ViewController) the user taps. They can select as many / few cells from each as they like.
Problem:
I'm crashing on any toggle besides the first cell. If I select the first cell in allHobbiesCV, I can select it again, and it will remove it from the myHobbiesCV. If I select that same cell again (to toggle it,) I crash. If I select any other cell besides the first, I crash.
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete item 7 from section 0 which only contains 3 items before the update'
Class Level
// Winter Hobbies
let allHobbiesArray = ["Skiing", "Snowboarding", "Drinking Bourbon", "Snow Shoeing", "Snowmobiling", "Sledding", "Shoveling Snow", "Ice Skating"]
var myHobbiesArray = [String]()
var allSelected = [IndexPath]()
var didSelectIPArray = [IndexPath]()
Data Source
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == allHobbiesCV {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ALL", for: indexPath) as! AllHobbiesCell
cell.allHobbiesLabel.text = allHobbiesArray[indexPath.item]
if cell.isSelected {
cell.backgroundColor = UIColor.green
}
else {
cell.backgroundColor = UIColor.yellow
}
return cell
}
else {
let cell = myHobbiesCV.dequeueReusableCell(withReuseIdentifier: "MY", for: indexPath) as! MyHobbiesCell
cell.myHobbiesLabel.text = myHobbiesArray[indexPath.item]
if cell.isSelected {
cell.backgroundColor = UIColor.red
}
else {
cell.backgroundColor = UIColor.yellow
}
return cell
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == allHobbiesCV {
return allHobbiesArray.count
}
else {
return myHobbiesArray.count
}
}
}
Delegate
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView == allHobbiesCV {
if myHobbiesArray.count <= 6
self.allSelected = []
didSelectIPArray.append(indexPath) // Store the selected indexPath in didSelectIPArray
myHobbiesArray.insert(allHobbiesArray[indexPath.item], at: 0)
let allHobbiesCell = allHobbiesCV.cellForItem(at: indexPath) as! AllHobbiesCell
allHobbiesCell.backgroundColor = UIColor.green
// Store all of the selected cells in an array
didSelectIPArray.append(indexPath)
myHobbiesCV.reloadData()
}
}
else {
let cell = myHobbiesCV.cellForItem(at: indexPath) as! MyHobbiesCell
cell.backgroundColor = UIColor.red
allSelected = self.myHobbiesCV.indexPathsForSelectedItems!
}
}
// Deselecting selected cells
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if collectionView == allHobbiesCV {
// Yellow is the unselected cell color. So let's change it back to yellow.
let allHobbiesCell = allHobbiesCV.cellForItem(at: indexPath) as! AllHobbiesCell
allHobbiesCell.backgroundColor = UIColor.yellow
// Remove (toggle) the cells. This is where I am stuck/crashing.
let indices = didSelectIPArray.map{ $0.item }
myHobbiesArray = myHobbiesArray.enumerated().flatMap { indices.contains($0.0) ? nil : $0.1 }
self.myHobbiesCV.deleteItems(at: didSelectIPArray)
myHobbiesCV.reloadData()
didSelectIPArray.remove(at: indexPath.item) // Remove the deselected indexPath from didSelectIPArray
}
else { // MyHobbies CV
let cell = myHobbiesCV.cellForItem(at: indexPath) as! MyHobbiesCell
cell.backgroundColor = UIColor.yellow
// Store the selected cells to be manually deleted.
allSelected = self.myHobbiesCV.indexPathsForSelectedItems!
}
}
}
Delete Button
#IBAction func deleteButtonPressed(_ sender: UIButton) {
for item in didSelectIPArray {
self.allHobbiesCV.deselectItem(at: item, animated: false) // Try deselecting the deleted items in allHobbiesCV... This is crashing.
}
}
The issue is how I am trying to toggle the allHobbiesCV cells in didDeselect. Was my approach correct in saving the selected cells in didSelectIPArray?
I'll be happy to provide further insight if needed. Thank you much in advance friends!
There is no need for all of that map and flatmap stuff. You can simply remove the object from the selected hobbies array:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView == allHobbiesCV {
if myHobbiesArray.count <= 6
self.allSelected = []
myHobbiesArray.insert(allHobbiesArray[indexPath.item], at: 0)
let allHobbiesCell = allHobbiesCV.cellForItem(at: indexPath) as! AllHobbiesCell
allHobbiesCell.backgroundColor = UIColor.green
myHobbiesCV.insertItems(at: [IndexPath(item: 0, section: 0)])
}
} else {
let cell = myHobbiesCV.cellForItem(at: indexPath) as! MyHobbiesCell
cell.backgroundColor = UIColor.red
allSelected = self.myHobbiesCV.indexPathsForSelectedItems!
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if collectionView == allHobbiesCV {
// Yellow is the unselected cell color. So let's change it back to yellow.
let allHobbiesCell = allHobbiesCV.cellForItem(at: indexPath) as! AllHobbiesCell
allHobbiesCell.backgroundColor = UIColor.yellow
let hobby = allHobbiesArray[indexPath.item]
if let index = myHobbiesArray.index(of:hobby) {
myHobbiesArray.remove(at: index)
myHobbiesCV.deleteItems(at: [IndexPath(item: index, section:0)])
}
} else { // MyHobbies CV
let cell = myHobbiesCV.cellForItem(at: indexPath) as! MyHobbiesCell
cell.backgroundColor = UIColor.yellow
// Store the selected cells to be manually deleted.
allSelected = self.myHobbiesCV.indexPathsForSelectedItems!
}
}
I currently have this (pseudo)code:
var selectedCell: UICollectionViewCell?
override func viewDidLoad() {
super.viewDidLoad()
...
#initialize all objects and pull data from the server to fill the cells
UIView.animate(withDuration: 0, animations: {
self.dataCollectionView.reloadData()
}, completion: {(finished) in
let indexPath = IndexPath(row: 0, section: 0)
self.dataCollectionView.selectItem(at: indexPath, animated: true, scrollPosition: .top)
self.collectionView(self.dataCollectionView, didSelectItemAt: indexPath)
})
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! DataCollectionViewCell
if !selectedCell {
cell.layer.borderWidth = 1
}
else {
cell.layer.borderWidth = 2
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAtindexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
selectedCell = cell
cell.image = SomeImageFromServer
cell.layer.borderWidth = 2
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell.layer.borderWidth = 1
}
My thinking is that this code will select the first cell right after the collection view has been loaded, and it does. The problem is it selects the last cell as well, but didSelectItemAtindexPath is never called for the last cell, and only the first cell.
I've tried selecting the second cell by using let indexPath = IndexPath(row: 1, section: 0) and it does select the second cell once the collectionview has been loaded, and the last cell is not selected as you would think.
And once any cell is selected, the last cell is unselected.
So my hypothesis is that this isn't the code thinking the cell is "selected" but that it's for some reason giving the selected cell a "selected cell border" but only when the first selected cell is the first one. Any thoughts?
Try moving the border setting into cell, UICollectionView will automatically manage the border width:
//Swift3
class TestCollectionViewCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
self.layer.borderWidth = 1 //Default border width
}
override var isSelected: Bool {
didSet{
if self.isSelected {
self.layer.borderWidth = 2
}
else{
self.layer.borderWidth = 1
}
}
}
}