Swift - Toggling CollectionView cells to toggle items in an Array - ios

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!
}
}

Related

Save selected state of UICollectionView inside UITableViewCell when dismiss the view controller

How can I save the selected state of UICollectionView inside an UITableViewCell?
For more details, I have an UITableView with 5 sections and each section has only 1 cell, I put another UICollectionView into the cell of the table view and whenever I select an item of collection view cell it will highlight with a red background.
Now I wanna save the selection state for the collection view even if I dismiss the view controller then open it again it must display the correct selected item and I think I will use UserDefaults for saving. But I noticed that when I select an item of collection view in another section it always saves the same index with the first section of the table view.
Here is my code for saving the selected index path to an array, can you please tell me where's my mistake:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let strData = itemFilter[indexPath.section].value[indexPath.item]
let cell = collectionView.cellForItem(at: indexPath) as? SDFilterCollectionCell
cell?.filterSelectionComponent?.bind(title: strData.option_name!, style: .select)
cell?.backgroundColor = .red
cell?.layer.borderColor = UIColor.white.cgColor
arrSelectedIndex.append(indexPath)
}
and when deselect:
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let strData = itemFilter[indexPath.section].value[indexPath.item]
let cell = collectionView.cellForItem(at: indexPath) as? SDFilterCollectionCell
cell?.filterSelectionComponent?.bind(title: strData.option_name!, style: .unselect)
cell?.backgroundColor = .white
cell?.layer.borderColor = UIColor.black.cgColor
if arrSelectedIndex.count > 0 {
arrSelectedIndex = arrSelectedIndex.filter({$0 != indexPath})
}else {
arrSelectedIndex.removeAll()
}
}
As you mention you want to save arrSelectedIndex in userdefault, so get arrSelectedIndex from userdefault.
if you have collectionView in each section of UITableView then with arrSelectedIndex save the indexpath of table section as well.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.cellForItem(at: indexPath) as? SDFilterCollectionCell else {
return UICollectionViewCell()
}
// color the background accordingly
if arrSelectedIndex.contains(indexPath) {
// selected state
cell.backgroundColor = .red
} else {
// non-selected state
cell.backgroundColor = .white
}
cell.layer.borderColor = UIColor.white.cgColor
return cell
}

UICollectionView - Select all cells doesn't update properly

I have a button which selects all cells in the collectionview. Once clicked, the button function changes so that all cells will be de-selected upon pressing it again.
So far so good.. But
1) When you select all cells with the button, scroll a bit down and to the top again
2) Then de-select all cells with the button, and select all cells with the button again
3) And start scrolling down, some cells (mostly 1-2 complete rows, later cells are fine again) are not properly updated, so they don't appear with the selected state which is a different background color. Seems like an issue with dequeueReusableCell, but I can't wrap my head around it..
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if cell.isSelected {
cell.backgroundColor = UIColor.green
} else {
cell.backgroundColor = UIColor.white
}
if cell.viewWithTag(1) != nil {
let cellTitle = cell.viewWithTag(1) as! UILabel
cellTitle.text = String(indexPath.row)
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) {
cell.backgroundColor = UIColor.green
selectedCells.append(indexPath.row)
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) {
cell.backgroundColor = UIColor.white
selectedCells.removeObject(indexPath.row)
}
}
And the action method for handling button clicking
#IBAction func selectButtonTapped(_ sender: Any) {
if isSelectAllActive {
// Deselect all cells
selectedCells.removeAll()
for indexPath: IndexPath in collectionView!.indexPathsForSelectedItems! {
collectionView!.deselectItem(at: indexPath, animated: false)
collectionView(collectionView!, didDeselectItemAt: indexPath)
let cell: UICollectionViewCell
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CVCell", for: indexPath)
}
selectButton.title = "Select all"
isSelectAllActive = false
} else {
// Select all cells
for i in 0 ..< collectionView!.numberOfItems(inSection: 0) {
collectionView!.selectItem(at: IndexPath(item: i, section: 0), animated: false, scrollPosition: UICollectionViewScrollPosition())
collectionView(collectionView!, didSelectItemAt: IndexPath(item: i, section: 0))
}
selectedCells.removeAll()
let indexPaths: [IndexPath] = collectionView.indexPathsForSelectedItems!
for item in indexPaths {
selectedCells.append(item.row)
}
selectedCells.sort{$0 < $1}
selectButton.title = "Select none"
isSelectAllActive = true
}
}
And for completion the array extension for removing an object
extension Array where Element : Equatable {
mutating func removeObject(_ object : Iterator.Element) {
if let index = self.index(of: object) {
self.remove(at: index)
}
}
}
Complete Xcode project can be found here: https://www.dropbox.com/s/uaj1asg43z7bl2a/SelectAllCells.zip
Used Xcode 9.0 beta 1, with iOS11 Simulator/iPhone SE
Thanks for your help!
Your code is a little confused because you are trying to keep track of cell selection state both in an array and in the cell itself.
I would just use a Set<IndexPath> as it is simpler and more efficient than an array. You can then refer to this set when returning a cell in cellForItemAt: and you don't need to do anything in willDisplay.
When you select/deselect all you can just reload the whole collection view and when an individual cell is selected/deselected, just reload that cell.
#objcMembers
class MainViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet var collectionView: UICollectionView!
#IBOutlet var toolBar: UIToolbar?
#IBOutlet weak var selectButton: UIBarButtonItem!
var selectedCells = Set<IndexPath>()
var isSelectAllActive = false
// MARK: - Classes
override func viewDidLoad() {
super.viewDidLoad()
// Collection view
collectionView!.delegate = self
collectionView!.dataSource = self
collectionView!.allowsMultipleSelection = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
#IBAction func selectButtonTapped(_ sender: Any) {
if isSelectAllActive {
// Deselect all cells
selectedCells.removeAll()
selectButton.title = "Select all"
isSelectAllActive = false
} else {
// Select all cells
for i in 0 ..< collectionView!.numberOfItems(inSection: 0) {
self.selectedCells.insert(IndexPath(item:i, section:0))
}
selectButton.title = "Select none"
isSelectAllActive = true
}
self.collectionView.reloadData()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 50
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: UICollectionViewCell
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CVCell", for: indexPath)
if self.selectedCells.contains(indexPath) {
cell.backgroundColor = .green
} else {
cell.backgroundColor = .white
}
if cell.viewWithTag(1) != nil {
let cellTitle = cell.viewWithTag(1) as! UILabel
cellTitle.text = String(indexPath.row)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("\ndidSelectItemAt: \(indexPath.row)")
if selectedCells.contains(indexPath) {
selectedCells.remove(indexPath)
} else {
selectedCells.insert(indexPath)
}
self.collectionView.deselectItem(at: indexPath, animated: false)
self.collectionView.reloadItems(at: [indexPath])
print("selectedCells: \(selectedCells)")
}
}

Displaying checkmark on collectionview

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])
}

Strange behavior with UICollectionView cell selection

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
}
}
}
}

Multiple UICollectionViewCell color gets changed together issue

I have created a UICollectionView with 12 cells in it. I would like to have their color changed (to the same color) on tap.
Here is my code :
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath)as! InterestCollectionViewCell
print("2 \(cell.interest) \(indexPath.row)")
if cell.selected == true {
cell.backgroundColor = UIColor.redColor()
}
else {
cell.backgroundColor = UIColor.clearColor()
}
}
Issues
Color doesn't change back when tapped again
When I tap on the [0] cell, the [5] and [10] cells changes color as well. Same after I tap on the [1] cell, [6]and [11] cells get called too...etc.m
Instead of setting color in didSelectItemAtIndexPath set the color in cellForItemAtIndexPath for that you need to declare instance of Int and store the row of collectionView inside that instance like this.
var selectedRow: Int = -1
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath:NSIndexPath)->UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("CELL", forIndexPath: indexPath) as! InterestCollectionViewCell
// Set others detail of cell
if self.selectedRow == indexPath.item {
cell.backgroundColor = UIColor.redColor()
}
else {
cell.backgroundColor = UIColor.clearColor()
}
return cell
}
Now in didSelectItemAtIndexPath set the selectedRow reload the collectionView.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if self.selectedRow == indexPath.item {
self.selectedRow = -1
}
else {
self.selectedRow = indexPath.item
}
self.collectionView.reloadData()
}
Edit: For multiple cell selection create one array of indexPath and store the object of indexPath like this.
var selectedIndexPaths = [NSIndexPath]()
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath:NSIndexPath)->UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("CELL", forIndexPath: indexPath) as! InterestCollectionViewCell
// Set others detail of cell
if self.selectedIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.redColor()
}
else {
cell.backgroundColor = UIColor.clearColor()
}
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if self.selectedIndexPaths.contains(indexPath) {
let index = self.selectedIndexPaths.indexOf(indexPath)
self.selectedIndexPaths.removeAtIndex(index)
}
else {
self.selectedIndexPaths.append(indexPath)
}
self.collectionView.reloadData()
}

Resources