Invalid update: invalid number of items in section 0. - swift

Recently I got the following error:
Fatal Exception: NSInternalInconsistencyException
Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (13) must
be equal to the number of items contained in that section before the
update (12), plus or minus the number of items inserted or deleted
from that section (0 inserted, 0 deleted) and plus or minus the number
of items moved into or out of that section (0 moved in, 0 moved out).
The error occurs in the following code in my tvOS client:
let removedIndexPaths = removedIndexes.map({ IndexPath(row: $0, section: 0) })
let addedIndexPaths = addedIndexes.map({ IndexPath(row: $0, section: 0) })
let updatedIndexPaths = updatedIndexes.map({ IndexPath(row: $0, section: 0) })
self.collectionView?.performBatchUpdates({
self.collectionView?.deleteItems(at: removedIndexPaths)
self.collectionView?.insertItems(at: addedIndexPaths)
}, completion: { _ in
guard let collectionView = self.collectionView else {
return
}
for indexPath in updatedIndexPaths {
if let myCell = collectionView.cellForItem(at: indexPath) as? MyCollectionViewCell {
let item = self.dataManager.items[indexPath.row]
myCell.updateUI(item)
}
}
let collectionViewLayout = self.collectionViewLayoutForNumberOfItems(self.dataManager.items.count)
if collectionViewLayout.itemSize != self.collectionFlowLayout.itemSize {
collectionView.setCollectionViewLayout(collectionViewLayout, animated: false)
}
})
I am only using one section in my collection view:
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
I have checked out couple of posts on the same topic, but they have not solved my problem, my guess is that the problem is in the following two lines, but I am not sure:
self.collectionView?.deleteItems(at: removedIndexPaths)
self.collectionView?.insertItems(at: addedIndexPaths)
Please help.

The call to insertItems(at:) and deleteItems(at:) must be accompanied with change in the datasource as well.
So, before calling these APIs, you would want to change your datasource, i.e. add objects into it before calling insertItems and remove objects from it before calling deleteItems

Found a very nice article about UICollectionView invalid number of items crash problem - https://fangpenlin.com/posts/2016/04/29/uicollectionview-invalid-number-of-items-crash-issue/
The item count returned by collectionView(_:numberOfItemsInSection:) should be sync with the updates made inside the closure. With this idea in mind, it’s easy to solve, just add a property as the item count and update it inside performBatchUpdates closure
func updateItems(updates: [ItemUpdate]) {
collectionView.performBatchUpdates({
for update in updates {
switch update {
case .Add(let index):
collectionView.insertItemsAtIndexPaths([NSIndexPath(forItem: index, inSection: 0)])
itemCount += 1
case .Delete(let index):
collectionView.deleteItemsAtIndexPaths([NSIndexPath(forItem: index, inSection: 0)])
itemCount -= 1
}
}
}, completion: nil)
}
and for the collectionView(_:numberOfItemsInSection:), instead of returning items.count, we return the property which is manually maintained by performBatchUpdates closure.
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return itemCount
}

Related

how to remove the cell from uitableview cell

Im trying to dynamically arranging table view when user select "type 3". It works when user select "type 3", "type 3-1" would be added in the tableview. However the program crashed when user select other than type3-1. I dont know how can I execute the "rows.remove(at:2)" before the override function is called. Any suggestion would appreciate!
class GuestViewController: UITableViewController {
var rows:[[[String:Any]]] = [[["type":RowType.DetailContent,
"subType":DCType.DCRightContent,
"name":CPFFields.CID,
"content":"9637"],
["type":RowType.DetailContent,
"subType":DCType.DCInput,
"name":CPFFields.VISIA]],
[["type":RowType.DetailTextView,
"CPFType":CPFFields.UV,
"title":CPFFields.preferenceTitle]],
[["type":RowType.DetailContent,
"subType":DCType.DCSelection,
"name":CPFFields.Phototherapy,
"title":CPFFields.anestheticTitle],
["type":RowType.DetailTextView,
"CPFType":CPFFields.Phototherapy,
"title":CPFFields.preferenceTitle]],
]
var isNewGuestSelected : Bool = false
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rows[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = rows[indexPath.section][indexPath.row]
let type = item["type"] as! RowType
if type == RowType.DetailContent
{
let cell = tableView.dequeueReusableCell(withIdentifier: "DetailNameCell", for: indexPath) as! DetailContentCell
let cpfType = item["name"] as? CPFFields ?? .Customer
cell.name.text = CPFFields.localizedString(from: cpfType)
if let field = item["title"] as? CPFFields
{
cell.name.text = CPFFields.localizedString(from: field)
}
cell.moreSlectionLeftSpace = true
var content:String? = ""
cell.type = cpfType
switch cpfType {
case .CID:
content = (profile?.birthDate.dateFromDateString?.stringForPaitentId ?? "") + (profile?.name ?? "")
case .CT:
content = ""
if let profile = profile
{
content = CPFCustomerType.localizedString(from: profile.type)
//New Guest
if(content == CPFCustomerType.type1.rawValue){
rows[0].insert(["type":RowType.DetailContent,
"subType":DCType.DCRightContent,
"name":CPFFields.CID,
"content":"9637"], at: 1)
isNewGuestSelected = true
} else{
if isNewGuestSelected == true{
rows[0].remove(at: 1)
isNewGuestSelected = false
}
}
}
let subType = item["subType"] as! DCType
cell.setcontentType(type: subType, content: content)
return cell
}
I expected not to see "rows[0][2]" after running "rows[0].remove(at:1)".
However the log is printing
rows[0][0]
rows[0][1]
rows[0][2]
then
it crashed at "let item = rows[indexPath.section][indexPath.row]"
because it is out of range
You are modifying your content while rendering, thus after numberOfRows:inSection: was called. Therefore the tableView is trying to access an element that no longer exists, since you removed it.
Your cycle:
→ number of rows 4
→ removed item, contents now has 3 items
→ cell for item 0
→ cell for item 1
→ cell for item 2
- cell for item 3 → crash
Consider replacing the logic you have here outside of the cellForRow method, and doing these operations before you reload your tableView.
You should use the tableView:cellForRow:atIndexPath strictly for dequeueing your cells and configuring them; not for modifying the underlying data store since funky things like you're experiencing now can happen.
If you provide a bit more context I can probably tell you where to place your code to fix this issue.
Actually, the solution is quite simple. I just added tableView.reloadData() after removing the array, and the UI can then be updated.
if isNewGuestSelected == true{
rows[0].remove(at: 1)
isNewGuestSelected = false
tableView.reloadData()
}

Search Bar crashing app when inputting characters

I have a UITableView that is populating locations and a Search Bar set as the header of that UITableView.
Whenever certain characters are entered, or a certain amount of characters are entered, the app crashes, giving me no error code.
Sometimes the app crashes after inputting one character, maybe 2 characters, maybe 3, or maybe 4. There seems to be no apparent reason behind the crashing.
The search function properly searches and populates the filtered results, but for no apparent reason, crashes if a seemingly arbitrary amount of characters are inputted.
I have tried using the exception breakpoint tool already, and it is providing me with no new information. I think it has something to do with if there are no search results.
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search Locations..."
navigationItem.hidesSearchBarWhenScrolling = false
searchController.hidesNavigationBarDuringPresentation = false
locationTableView.tableHeaderView = searchController.searchBar
searchController.searchBar.sizeToFit()
searchController.searchBar.showsCancelButton = false
searchController.searchBar.barTintColor = UIColor.white
filteredData = locationList
// Sets this view controller as presenting view controller for the search interface
definesPresentationContext = true
locationList = createArray()
// Reload the table
let range = NSMakeRange(0, self.locationTableView.numberOfSections)
let sections = NSIndexSet(indexesIn: range)
self.locationTableView.reloadSections(sections as IndexSet, with: .fade)
}
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String) {
filteredData = locationList.filter({( locationName : Location) -> Bool in
return locationName.locationName.lowercased().contains(searchText.lowercased())
})
let range = NSMakeRange(0, self.locationTableView.numberOfSections)
let sections = NSIndexSet(indexesIn: range)
self.locationTableView.reloadSections(sections as IndexSet, with: .fade)
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
func locationTableView(_ locationTableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
return filteredData.count
}
return locationList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let locationCell = locationTableView.dequeueReusableCell(withIdentifier: "locationCell", for: indexPath) as! locationCell
let location: Location
if isFiltering() {
location = filteredData[indexPath.row]
} else {
location = locationList[indexPath.row]
}
locationCell.setLocation(location: location)
return locationCell
}
The expected result is that the UITableView should populate with filtered results. Instead, it populates them and crashes if too many characters are inputted (usually 1-4 characters).
EDIT 1: I have found through debugging the error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
appears on Line 2 on this block of code:
if isFiltering() {
location = filteredData[indexPath.row]
} else {
location = locationList[indexPath.row]
}
EDIT 2: This is the tutorial I used.
https://www.raywenderlich.com/472-uisearchcontroller-tutorial-getting-started
Seems like you are expecting the tableView to provide YOU with the number of sections... it is supposed to be driven by your own datasource.
Since you are not providing a numberOfSections in your data source I'm assuming it is 1. If all of your rows are in 1 section, all of the nifty reloading you are doing could be greatly simplified.
I suggest you read up on UITableView dataSource protocol at https://developer.apple.com/documentation/uikit/uitableviewdatasource
Reviewing the tutorial you are reading, it seems it is using a reloadData() which forces the tableView to ignore previous number of rows and reload its content with a new number of rows. And based on your findings so far, I would assume that is part of the root cause, with the tableview wrongly assuming a pre-determined number of rows and attempting to retrieve cells that are no longer within range.

Let only select specific cells with shouldSelectItemAt indexPath function

I want that only four specific cells can be selected at one time. When a button is pressed, I want that the selectable cells are 4 indexPath.row lower.
Example: In the beginning, indexPath.row 44-47 is selectable. If the button is pressed I want, that the indexPath.row 40-43 is selectable and so on.
I thought about making an array with the indexPath and If the button is pressed, the numbers in the array are 4 numbers lower.
Than I don't know, how to add this to the shouldSelectItemAt indexPath function.
How can I realize this?
You can use an IndexSet.
var allowedSelectionRow: IndexSet
allowedSelectionRow.insert(integersIn: 44...47) //Initial allowed selection rows
In collectionView(_:shouldSelectItemAt:)
return allowedSelectionRow.contains(indexPath.row) //or indexPath.item
Whenever you need:
allowedSelectionRow.remove(integersIn: 44...47) //Remove indices from 44 to 47
allowedSelectionRow.insert(integersIn: 40...43) //Add indices from 40 to 43
Advantage from an Array: Like a set, there is unicity of the values (no duplicates). Contains only integers, and you can add in "range" which can be useful (not add all the indices, but a range).
After comments, if you have only 4 rows allowed and consecutive, you can have that method:
func updateAllowedSectionSet(lowerBound: Int) {
let newRange = lowerBound...(lowerBound+3)
allowedSectionRow.removeAll() //Call remove(integersIn:) in case for instance that you want always the 1 row to be selectable for instance
allowedSectionRow.insert(integersIn: newRange)
}
For the first one, you just need to do:
updateAllowedSectionSet(lowerBound: 44) instead of allowedSelectionRow.insert(integersIn: 44...47)
Let's consider that the items form a String array, and you are keeping track of the selected indices as a Range.
var selectedRange: Range<Int>? {
didSet {
collectionView.reloadData()
}
}
var items: [String] = [] {
didSet {
// To make sure that the selected indices are reset everytime this array is modified,
// so as to make sure that nothing else breaks
if items.count >= 4 {
// Select the last 4 items by default
selectedRange = (items.count - 4)..<items.count
} else if !items.isEmpty {
selectedRange = 0..<items.count
} else {
selectedRange = nil
}
}
}
Then, when you are pressing the button to decrement the range, you can use this logic to handle the same:
func decrementRange() {
if var startIndex = selectedRange?.startIndex,
var endIndex = selectedRange?.endIndex {
startIndex = max((startIndex - 4), 0)
endIndex = min(max((startIndex + 4), (endIndex - 4)), items.count)
selectedRange = startIndex..<endIndex
}
}
Then, you can identify whether the the selection is being done on the active range using:
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
if let selectedRange = selectedRange {
return selectedRange.contains(indexPath.item)
}
return false
}
Note: I would advice you to verify whether this covers all the corner cases before trying it out for production code.

How to reload section from collectionview

I tried to delete a cell from collectionview on didSelect method.
Deleting the data is working well.
But I'm getting this:
reason: 'Invalid update: invalid number of sections. The number of
sections contained in the collection view after the update (1) must be
equal to the number of sections contained in the collection view
before the update (2), plus or minus the number of sections inserted
or deleted (0 inserted, 0 deleted).'
This is delete cell function:
func deleteMovie(cell: MoiveCollectionViewCell) {
var indexPath: NSIndexPath = self.collectionView!.indexPathForCell(cell)!
// remove and reload data
self.collectionView.deleteItemsAtIndexPaths([indexPath])
self.collectionView.reloadSections(NSIndexSet(index: indexPath.section))
}
and numberOfSectionsInCollectionView func :
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
//number_per_section = 3
if manager.movie_List.count % number_per_section == 0
{
return manager.movie_List.count / number_per_section
}
else {
return manager.movie_List.count / number_per_section + 1
}
}
I just want to reload number of sections. What should I add?
You need to update the datasource first before you make the calls to:
collectionView.deleteItemsAtIndexPaths([indexPath])
collectionView.reloadSections(NSIndexSet(index: indexPath.section))
So first delete the object in your datasource model (array, dictionary etc).
Then you can just do this:
let indexSet = IndexSet(integer: indexPath.section)
collectionView.reloadSections(indexSet)
The problem is you are just removing a cell from the section and that will definitely not work.
Once you have removed your cell you need to notify your manager.movie_List array too. So, remove the selectedIndex data from array and then try it out!

SIGABRT when selecting rows in TableViewController

I have a problem:
When I select the 2nd cell of my tableView, I get a SIGABRT. But when I select the first one, everything works just fine.
It must have to do with my TableViewController Class, because "SET ROWS IN SECTION" (from my print statement) will be printed 3 times, when I select the 2nd cell, but 3 * 4 = 12 times, when I select the first one...Oh... and I get this error:
GONNA SET THE ROWS IN SECTION
2
SET SECTION 2 TO 1
GONNA SET THE ROWS IN SECTION
0
SET SECTION 0 TO 1
GONNA SET THE ROWS IN SECTION
1
SET SECTION 1 TO 3
2016-07-04 12:43:14.407 Sessions[1229:629535] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]'
*** First throw call stack:
(0x1822e6db0 0x18194bf80 0x1821c7098 0x187b7d5ac 0x187933f1c 0x18756d444 0x18752aff8 0x1878e4b88 0x187529fc4 0x187529d5c 0x187529b68 0x187794a20 0x18752fac8 0x18743fac8 0x187465350 0x18743ed40 0x182c18cc0 0x18743ebc4 0x18744c678 0x18763edbc 0x18743fac8 0x18743f7ac 0x18743ed40 0x182c18cc0 0x18743ebc4 0x18744c678 0x1876df8c0 0x1874535b4 0x18763e8d4 0x18775e334 0x18763e44c 0x1875f8818 0x187503e40 0x187503b1c 0x187503a84 0x1874401e4 0x184dd2994 0x184dcd5d0 0x184dcd490 0x184dccac0 0x184dcc820 0x1874365f8 0x18229c728 0x18229a4cc 0x18229a8fc 0x1821c4c50 0x183aac088 0x1874ae088 0x1000f8b54 0x181d628b8)
libc++abi.dylib: terminating with uncaught exception of type NSException
"SET SECTION 2 TO 1" means, that section 2 has 1 cell...
This is my class:
import UIKit
class SessionsTableViewController: UITableViewController {
let header = ["Mediatoren", "Medianden", "Konfliktverlauf"]
var medianden: Array<String> = []
var mediators: Array<String> = []
// ----------------
// MARK : IBACTIONS
// ----------------
(I just deleted these...)
// ---------------------
// MARK : OVERRIDE FUNCS
// ---------------------
override func viewDidLoad() {super.viewDidLoad() }
override func didReceiveMemoryWarning(){ super.didReceiveMemoryWarning()}
override func viewWillAppear(animated: Bool) {
print(sessions.workingOnSession)
medianden.append((sessions.workingOnSession["Name1"] as? String)!)
medianden.append((sessions.workingOnSession["Name2"] as? String)!)
medianden.append((sessions.workingOnSession["Name3"] as? String)!)
medianden.append((sessions.workingOnSession["Name4"] as? String)!)
//print(medianden)
for _ in 0...3 {
if medianden.last == "" {
medianden.removeLast()
}
}
print(medianden)
mediators.append((sessions.workingOnSession["MID"] as? String)!)
mediators.append((sessions.workingOnSession["MID2"] as? String)!)
//print(mediators)
for _ in 0...1 {
if mediators.last == "" {
mediators.removeLast()
}
}
print(mediators)
}
// ---------------------
// MARK : FUNCS
// ---------------------
(Deleted these too...)
// -----------------
// MARK : TABLE VIEW
// -----------------
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.header[section]
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 3
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("SET ROWS IN SECTION")
print(section)
if section == 0 {
print("SET SECTION 0 TO \(mediators.count)")
return mediators.count
} else if section == 1 {
print("SET SECTION 1 TO \(medianden.count)")
return medianden.count
} else if section == 2 {
print("SET SECTION 2 TO 1")
return 1
}
return 0
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("MAKE MY CELLS...")
let cell = UITableViewCell()
if indexPath.section == 0 {
cell.detailTextLabel?.text = mediators[indexPath.row]
return cell
} else if indexPath.section == 1 {
cell.textLabel!.text = medianden[indexPath.row]
return cell
} else if indexPath.section == 2 {
let textCell = FSTextViewTableViewCell()
return textCell
//textCell.textView.text = sessions.workingOnSession["Description"] as? String
}
return cell
}
}
thanks to everyone!!! :-) and sorry for my bad english...
Fabian
The error message is pretty clear.
uncaught exception 'NSRangeException',
reason: '*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]'
So you have an array with elements [0..1] inclusive and you're accessing element [2]. There are only so many arrays in your program: mediators, medianden, self.header. Try doing some more debugging to see which one is at fault.
It seems like you are assuming that mediators has 2 elements and mediaden has 4, and this is hard-coded. It's better to not sprinkle these dependencies throughout the code; instead, set the size once, and make the remainder of the code work independently of the size (with for loops etc).