Invalid UICollectionView Update - ios

I have a sectioned uicollectionview with nested cells that can be deleted on tap.
My goal is to display cells from the datasource if available, and if not, to show a "placeholder" cell stating no data is currently available.
My issue emerges on delete of a last remaining cell under a given section. My numberOfItemsInSection is 1 specifically for the "placeholder" cell but should be 0 to align to the datasource of 0 where no more data is available.
Any thoughts on workarounds?
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if sectionItems[section].count == 0 {
return 1
}
else {
return sectionItems[section].count
}
}
func onTap() {
self.sectionItems[indexPath.section]?.remove(at: indexPath.item)
self.exampleCollectionView.deleteItems(at: [indexPath])
}

When you delete your last item, you need to instead reload for the "No Items" cell. Something like this should work:
func onTap() {
sectionItems[indexPath.section]?.remove(at: indexPath.item)
let isEmpty = sectionItems[indexPath.section]?.count ?? 0 == 0
exampleCollectionView.performBatchUpdates({
if isEmpty {
self.exampleCollectionView.reloadItems(at: [indexPath])
} else {
self.exampleCollectionView.deleteItems(at: [indexPath])
}
}, completion: nil})
}
Essentially you need to tell the collectionView that there will still be one cell there after the update when the last item is removed.

Related

How to use single array in multiple collectionView sections?

I have single array which contain game`s informations. My Json has 12 items in a page. I did created 4 sections which has 3 rows. It is repeating first 3 items of array in every sections.
Screenshot from app
I want to use like that;
Total Items = 12
Section = 1 2 3
Section = 4 5 6
Section = 7 8 9
Section = 10 11 12
How can I do that ? Thanks in advance :)
func numberOfSections(in collectionView: UICollectionView) -> Int {
return id.count / 3
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "lastAddedCell", for: indexPath) as! lastAddedCell
cell.gameName.text = name[indexPath.row]
cell.gameImage.sd_setImage(with: URL(string:resimUrl[indexPath.row]))
return cell
}
I don't think it's such a good idea. I would rather create the section separately by making a section manager than making them from the same array. But, if you want to do it the way you are doing it right now. Here is an easy fix:
func numberOfSections(in collectionView: UICollectionView) -> Int {
return id.count / 3
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "lastAddedCell", for: indexPath) as! lastAddedCell
let index = indexPath.row + (indexPath.section * 3) // The index is then based on the section which is being presented
cell.gameName.text = name[index]
cell.gameImage.sd_setImage(with: URL(string:resimUrl[indexPath.row]))
return cell
}
Consider this case below and implement it,
var array = [1,2,3,4,5,6,7,8,9,10,11,12]
//Statcially you can slice them like this
var arr2 = array[0...2] {
didSet {
//reload your collection view
}
}
var arr3 = array[3...5]
var arr4 = array[6...8]
var arr5 = array[9...array.count - 1]
Above you manually sliced the dataSource for each UICollectionView but the problem is this is really risky and eventually can lead to an Index Out of Range crash, so we dynamically slice the array thru looping in it using the index of each element in range of +3 indexes to append to the new UICollectionView data source.
// loop thru the main array and slice it based on indexes
for(index, number) in array.enumerated() {
if 0...2 ~= index { // if in range
arr2.append(number)
} else
if index <= 5 {
arr3.append(number)
} else
if index <= 8 {
arr4.append(number)
} else
if index <= 11 {
arr5.append(number)
}
}
Finally : in your numberOfItemsInSection check the UICollectionView and set return its data source like,
if collectionView = myMainCollectionView {
return arr3.count
}
And goes the same for the cellForItemAt
Heads Up : make sure your dataSource arrays are empty initially,
let arr2: [Int] = [] {
didSet{
//reload your collectionView
}
}

Crash when moving item from one UICollectionView Section to another, then deleting that item

I have a collectionView that has two sections, each filling up cells with separate arrays.
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 && GlobalList.priorityArray.count != 0 {
return GlobalList.priorityArray.count
} else if section == 1 && GlobalList.listItemArray.count != 0 {
return GlobalList.listItemArray.count
} else {
return 0
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return sections
}
I can move items between sections without any issues. I also can delete items. My problem occurs when I move an item between sections, reload data, then try to delete all the items in that section, I get the following error.
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (3) must be equal to the number of items contained in that section before the update (3), plus or minus the number of items inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
Here are the methods I have created to deal with deleting and moving items
Moving Items:
override func collectionView(_ collectionView: UICollectionView,
moveItemAt sourceIndexPath: IndexPath,
to destinationIndexPath: IndexPath) {
if (sourceIndexPath as NSIndexPath).section == 0 {
listItem = GlobalList.priorityArray.remove(at: sourceIndexPath.item)
} else if (sourceIndexPath as NSIndexPath).section == 1 {
listItem = GlobalList.listItemArray.remove(at: sourceIndexPath.item)
}
if (destinationIndexPath as NSIndexPath).section == 0 {
GlobalList.priorityArray.insert(listItem, at: destinationIndexPath.item)
} else if (destinationIndexPath as NSIndexPath).section == 1 {
GlobalList.listItemArray.insert(listItem, at: destinationIndexPath.item)
}
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(reloadData), userInfo: nil, repeats: false)
}
Deleting Items:
func deleteItem(sender: UIButton) {
let point : CGPoint = sender.convert(.zero, to: collectionView)
let i = collectionView!.indexPathForItem(at: point)
print("deleting.. \(String(describing: i))")
let index = i
GlobalList.listItemArray.remove(at: (index?.row)!)
deleteItemCell(indexPath: i!)
}
func deleteItemCell(indexPath: IndexPath) {
collectionView?.performBatchUpdates({
self.collectionView?.deleteItems(at: [indexPath])
}, completion: nil)
}
Let me know if anything else is needed to figure this out.
Thanks in advance!
I also had a similar issue with the collectionView when calling reloadData and then directly after trying to insert cells into the collectionView. I solved this issue by instead manually deleting and inserting sections in the collectionView inside a perform batch updates, like so:
collectionView.performBatchUpdates({ [weak self] () in
self?.collectionView.deleteSections([0])
self?.collectionView.insertSections([0])
}, completion: nil)

UICollectionView attempts to dequeue cells beyond data source bounds while reordering is in progress

I have implemented collection view with cell reordering
class SecondViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private var numbers: [[Int]] = [[], [], []]
private var longPressGesture: UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
for j in 0...2 {
for i in 0...5 {
numbers[j].append(i)
}
}
longPressGesture = UILongPressGestureRecognizer(target: self, action: "handleLongGesture:")
self.collectionView.addGestureRecognizer(longPressGesture)
}
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case .Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case .Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case .Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
}
extension SecondViewController: UICollectionViewDataSource {
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return numbers.count
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let number = numbers[section].count
print("numberOfItemsInSection \(section): \(number)")
return number
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
print("cellForItemAtIndexPath: {\(indexPath.section)-\(indexPath.item)}")
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! TextCollectionViewCell
cell.textLabel.text = "\(numbers[indexPath.section][indexPath.item])"
return cell
}
func collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
let temp = numbers[sourceIndexPath.section].removeAtIndex(sourceIndexPath.item)
numbers[destinationIndexPath.section].insert(temp, atIndex: destinationIndexPath.item)
}
}
It works fine until I try to drag an item from section 0 to section 2 (which is off screen). When I drag the item to the bottom of collection view, it slowly starts scrolling down. At some point (when it scrolls past section 1) application crashes with fatal error: Index out of range because it attempts to request cell at index 6 in section 1 while there are only 6 items in this section. If I try to drag an item from section 1 to section 2, everything works fine.
Here's an example project reproducing this problem (credit to NSHint):
https://github.com/deville/uicollectionview-reordering
Is this a bug in framework or am I missing something? If it is the former, what would be the workaround?
I actually ran into this issue today. Ultimately the problem was in itemAtIndexPath - I was referencing the datasource to grab some properties for my cell : thus the source of the out of bounds crash. The fix for me was to keep a reference to the currently dragging cell and in itemAtIndexPath; check the section's datasource length versus the passed NSIndexPath and if out of bounds; refer to the dragging cell's property.
Likesuchas (in Obj-C ... haven't moved to Swift yet) :
NSArray* sectionData = [collectionViewData objectAtIndex:indexPath.section];
MyObject* object;
if (indexPath.row < [sectionData count]) {
object = [dataObject objectAtIndex:indexPath.row];
} else {
object = draggingCell.object;
}
[cell configureCell:object];
Not sure if this is similar to your exact issue; but it was mine as everything is working as expected now. :)

Set numberOfItemsInSection at Runtime - UICollectionView

I would like to set the numberOfItemsInSection of my collectionView at runtime. I will be changing the value programmatically at runtime quite often and would like to know how.
I have an array of images to display in my UICollectionView (1 image per UICollectionViewCell), and the user can change the category of the images to display, which will also change the number of images to display. When the view loads, the numberOfItemsInSection is set to the count of the allClothingStickers array. But number this is too high. The array that does get displayed is the clothingStickersToDisplay array which is a subset of the allClothingStickers array.
There is this error after some scrolling:
fatal error: Array index out of range
This is because the number of items has become smaller, but the UICollectionView numberOfItemsInSectionproperty has not changed to be a smaller number.
I have this function that sets the number of cells in the UICollectionView before runtime.
func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return self.numberOfItems
}
This function to set the stickersToDisplay array (and I want to update the numberOfItemsInSection property here):
func setStickersToDisplay(category: String) {
clothingStickersToDisplay.removeAll()
for item in self.allClothingStickers {
let itemCategory = item.object["category"] as! String
if itemCategory == category {
clothingStickersToDisplay.append(item)
}
}
self.numberOfItems = clothingStickersToDisplay.count
}
This is the function that returns the cell to display:
func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath)
-> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(
identifier,forIndexPath:indexPath) as! CustomCell
let sticker = clothingStickersToDisplay[indexPath.row]
let name = sticker.object["category"] as! String
var imageView: MMImageView =
createIconImageView(sticker.image, name: name)
cell.setImageV(imageView)
return cell
}
EDIT: Oh yeah, and I need to reload the UICollectionView with the new clothingStickersToDisplay at the same place that I update it's numberOfItemsInSection
I think what you should do is to clothingStickersToDisplay a global array declaration.
Instead of using that self.numberOfItems = clothingStickersToDisplay.count
Change this function to
func setStickersToDisplay(category: String) {
clothingStickersToDisplay.removeAll()
for item in self.allClothingStickers {
let itemCategory = item.object["category"] as! String
if itemCategory == category {
clothingStickersToDisplay.append(item) //<---- this is good you have the data into the data Structure
// ----> just reload the collectionView
}
}
}
In the numberOfItemsInSection()
return self.clothingStickersToDisplay.count

Inserting rows in UITableView upon click

I'm having trouble adding rows to the UITableView upon UIButton click.
I have two custom-cell xibs - one that contains an UILabel, another one that contains an UIButton.
Data for the table cell is loaded from two dictionaries (answersmain and linesmain).
Here is the code for the UITableView main functions:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.linesmain["Audi"]!.count + 1
}
// 3
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row < 3){
var cell:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
cell.lblCarName.text = linesmain["Audi"]![indexPath.row]
return cell
} else {
var celle:vwAnswers = self.tableView.dequeueReusableCellWithIdentifier("cell2") as! vwAnswers
celle.Answer.setTitle(answersmain["Good car"]![0], forState:UIControlState.Normal)
return celle
}}
What do I put here?
#IBAction func option1(sender: UIButton) {
// I need to add rows to the uitableview from two dictionaries into two different xibs
}
You can do the next:
var showingAll = false
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return showingAll ? self.linesmain["Audi"]!.count + 1 : 0
}
#IBAction func option1(sender: UIButton) {
showingAll = true
tableView.beginUpdates()
let insertedIndexPathRange = 0..<self.linesmain["Audi"]!.count + 1
var insertedIndexPaths = insertedIndexPathRange.map { NSIndexPath(forRow: $0, inSection: 0) }
tableView.insertRowsAtIndexPaths(insertedIndexPaths, withRowAnimation: .Fade)
tableView.endUpdates()
}
You should take a look over the documentation here
There is this UITableView method called insertRowsAtIndexPaths:withRowAnimation: that inserts row at a specified indexPath.
You need to modify linesmain and answersmain by adding data to these and then call [self.tableView reloadData].
It would be better if you extract linesmain["Audi"] and answersmain["Good car"] and save them into different mutable arrays and modify those.
You need to do this in the func option1.

Resources