Is there any way to delete the last row of a particular section in a UITableView without deleting that entire section? I would like to be able to remove the last row from a section but keep that section's header visible, as it indicates that the sections exists but has no data.
I've found multiple prior answers on StackOverflow, but their solutions all seem to be that you have to delete the section if you want to delete the last row of a section, e.g., How to delete the last row of a section?
Well, your problem is related how UITableViewDataSource protocol works, if there isn't any content in the section, the section shouldn't be displayed.
But what you can do about it?
There is a simple way, in your dataSource numberOfRowsInSection you can do an if, that condition will verify if your dataSource count is equal to zero and return 1 instead, like this:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var numberOfRows = 0
switch section{
case 0:
numberOfRows = firstSectionArray.count()
case 1:
numberOfRows = secondSectionArray.count()
default:
()
}
if numberOfRows == 0 {
numberOfRows = 1
}
return numberOfRows
}
And in the cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var numberOfRowsForThisSection = 0
switch indexPath.section{
case 0:
numberOfRowsForThisSection = firstSectionArray.count()
case 1:
numberOfRowsForThisSection = secondSectionArray.count()
default:
()
}
if numberOfRowsForThisSection == 0 {
return tableView.dequeueReusableCellWithIdentifier("blankCell") as! UITableViewCell
}
// Do the default implementation
// .
// .
// .
}
"Oh, but now there is a white cell in the middle of the tableView"
Ok, let's break that:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
var numberOfRowsForThisSection = 0
switch indexPath.section{
case 0:
numberOfRowsForThisSection = firstSectionArray.count()
case 1:
numberOfRowsForThisSection = secondSectionArray.count()
default:
()
}
if numberOfRowsForThisSection == 0 {
return 0.0
}
}
...and it turns out that my error was elsewhere. In this case, I was accidentally trying to delete index path (1, 2147483647). This error resulted because I was assuming that the array I was using for that section's data contained the object I wanted to delete. Since it didn't, [array indexOfObject:object] equaled 2147483647. Oddly enough, the error produced was Invalid number of sections rather than Invalid number of rows in section.
Related
I am building an iOS app, basically the user create an item by pressing the "+" button and then the app should put the new item in according section of the table based on the location of the item. However I got the error: Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update. Here is my code, thank you
let sections = ["Bathroom", "Bedroom", "Dining Room", "Garage", "Kitchen", "Living Room"]
#IBAction func addNewItem(_ sender: UIBarButtonItem) {
/*
//Make a new index path for the 0th section, last row
let lastRow = tableView.numberOfRows(inSection: 0)
let indexPath = IndexPath(row: lastRow, section: 0)
//insert this new row into the table
tableView.insertRows(at: [indexPath], with: .automatic)*/
//Create a new item and add it to the store
let newItem = itemStore.createItem()
var Num: Int = 0
if let index = itemStore.allItems.index(of: newItem) {
switch newItem.room {
case "Bathroom":
Num = 0
print("I am in here")
case "Bedroom":
Num = 1
case "Dining Room":
Num = 2
case "Garage":
Num = 3
case "Kitchen":
Num = 4
case "Living Room":
Num = 5
default:
Num = 0
print("I am in Default")
}
let indexPath = IndexPath(row: index, section: Num)
tableView.insertRows(at: [indexPath], with: .automatic)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemStore.allItems.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//create an instance of UITableViewCell, with default appearance
//let cell = UITableViewCell(style: .value, reuseIdentifier: "UITableViewCell")
//get a new or recycled cell
//if indexPath.row < itemStore.allItems.count {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
//Set the text on the cell with the decription of the item
//that is at the nth index of items, where n = row this cell
//will appear in on the table view
let item = itemStore.allItems[indexPath.row]
if item.name == "" {
cell.nameLabel.text = "Name"
} else {
cell.nameLabel.text = item.name
}
if item.serialNumber == nil {
cell.serialNumberLabel.text = "Serial Number"
} else {
cell.serialNumberLabel.text = item.serialNumber
}
cell.valueLabel.text = "$\(item.valueInDollars)"
cell.roomLabel.text = item.room
if item.valueInDollars < 50 {
cell.valueLabel.textColor = UIColor.green
}else if item.valueInDollars >= 50 {
cell.valueLabel.textColor = UIColor.red
}
cell.backgroundColor = UIColor.clear
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let LocationName = sections[section]
return LocationName
}
thank you so much for your time!
and this is how I create item
#discardableResult func createItem() -> Item {
let newItem = Item(random: false)
allItems.append(newItem)
return newItem
}
The problem is that you should insert the item in the array first:
itemStore.allItems.append(newItem)
Also, there is a difference between sections and rows in numberOfRowsInSection(return number of rows for every section) you have a switch that returns the same number, it should be
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemStore.allItems.count
}
Edit :
The problem is when the table loads there is 0 rows for all the sections ( itemStore.allItems.count is zero ), when you try to insert a row say at section 0 , row 0 -- the dataSource must be updated only for that section , which is not happen in your case as it's the same array that is returned for all sections , so you must either have an array of array where inner array represent number of rows so addition/deletion from it doesn't affect other ones ,,,,, or lock the insert to say section 0 like this
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0 ) {
return itemStore.allItems.count
}
return 0
}
in this edit i inserted in 2 sections 0 and 2 with no crash because i handled numberOfRowsInSection to return old numbers for old section that why to be able to insert in all sections you must have a different data source array or manage from numberOfRowsInSection , see edited demo here Homepwner
Instead of setting footer in viewDidLoad implement this method
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
if(section == 5 ) {
let textLabel = UILabel()
textLabel.text = "No more Item"
return textLabel
}
return nil
}
There is something terribly wrong with your data source. Your numberOfRow for all sections is the same. i.e. itemStore.allItems.count. Which means you are setting the same number of rows in each section.
The main issue is, while adding a new item, you are inserting a new row in the specific section which means
new number of rows for section = previous number of rows in section +
1
However, there is no addition in the data source.
So according to the code, you have inserted a new row, but your data source has one record less since you didn't add the item there.
So I would like you do the following in order of these steps :
Add item in required data source.
inserRowInSection
Update : Remove the condition in cellForRowAtIndexPath :
if indexPath.section <= 1 and it's else block. You don't need that. If number of sections is less than 1, it won't be called. Moreover it's the else block which is getting called all the time because you already have sections in your code. So if case will never be called.
So I got the issue. In itemStore.createItem() the value for room is always Room. In the switch-case, you never have Room case. So it always falls in default case. You need to fix that.
#Jacky.S I downloads your code and try to debug it.Fist look our error properly.
reason: 'Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (0)
So It says before update, your sections has 0 rows. Now you try to update you tableview using this piece of code.
print(indexPath)
tableView.insertRows(at: [indexPath], with: .automatic)
I just added the print statement for debugging purpose and it prints [0, 0] .So you say your tableview to insert 0th row on section 0.
Now when you insert any row into tableview it calls the delegate method.Now look at the below delegate method.
override func tableView(_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int {
if section == 0{
print("0",itemStore.allItems.count)
}
if section == 1{
print("1",itemStore.allItems.count)
}
// print(itemStore.allItems)
return itemStore.allItems.count
}
In your code you just return itemStore.allItems.count.I added the above code for debugging.Now look that itemStore.allItems.count always return 1.So for first section it works perfectly because few times ago you insert a row in first section whose index is [0 = row, 0 = section].So you previously returned rows for section 0 is equal to the recently return rows for section 0.But When it comes for section 1 you previously return no row that means your section 1 has no rows in previous but in your above delegate method you also return 1 row for section 1.Which is conflicting because in previous you never update or return any rows for section 1.That's why you code crash and and say that
your number of rows contained in an existing section after the update is 1 which must be equal to the number of rows contained in that section before the update. Which is 0.
So this is the explanation for you crash.
Now, to recover from crash your allItems should be a dictionary or an array of array.
var allItems = [[Item]]()
or
var allItems = ["String":[Item]]()
guess allItems element is something like this
allItems = ["bathroom":[item1, item2, item3],"bedroom":[item1, item2, item3, item4]]
so here you have 2 section one is bathroom and another is bedroom.First section have 3 row and second one have 4 row and in you tableview delegate method you have to return 3 for section 0 and 4 for section 1
So I've my app crashing everytime I try to delete the last cell of the section.
Ex., if my section has 10 rows, I can delete them without any problem but the last one throws the following error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (1) must be equal to the number of sections contained in the table view before the update (3), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).'
I've searched around here and found some ways to fix this problem, but I tried them all and couldn't get this issue fixed, it still crashes and throws the same error.
The related parts of my code goes as it follows:
override func numberOfSections(in tableView: UITableView) -> Int {
if (main_arr.count > 0 && sub_arr.count > 0) {
self.numberOfSections = 3
} else {
self.numberOfSections = 2
}
return self.numberOfSections
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0) {
return 1
} else if (section == 1 && main_arr.count > 0) {
return main_arr.count
} else {
return sub_arr.count
}
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
switch indexPath.section {
case 1:
main_arr.remove(at: indexPath.row)
case 2:
sub_arr.remove(at: indexPath.row)
default:
print("default 001")
}
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
EDIT 1
I tried to handle the global variable, responsible for the numberOfSections so when any of my arrays.count == 0 it will be decreased by 1, but it didn't solved the issue.
I do understand completely the error message, like, if I've 3 sections, and I delete the whole content of one of them, I should delete one section and delete it from the datasource as well.
The problem is that numberOfSections returns different values before and after you delete rows, but you don't delete any sections. So you should either return a constant value in numberOfSections or call deleteSections in addition to deleteRows
The main thing to remember is the following:
UITableView must always contain the same number of rows and sections as your dataSource.
You cannot just return a new value in numberOfSections or numberOfRows dataSource methods. Every change should be compensated with delete/insert rows(sections) method. And vice versa: if you delete/insert to tableView, you must return corresponding values in dataSource methods. Just as your exception message states:
The number of sections contained in the table view after the update
(1) must be equal to the number of sections contained in the table
view before the update (3), plus or minus the number of sections
inserted or deleted (0 inserted, 0 deleted).
It's because 3+0≠1. In this case you should have deleted two sections to avoid crash.
The error message is actually very helpful. It talks about sections - it expected not to change, but it found a different value after your operation.
Basically, UIKit did check the number of sections and rows before calling your code, runs your code, and then checks if the the new numbers line up with your operations. In you case, it doesn't - because you are calling deleteRows (which should decrease the row count for the given section), but your numberOfSections delegate gives a different result now. So you either want to call deleteSections or keep the sectionCount unmodified.
BTW, it is not forbidden to return 0 as the number of rows for a section - maybe this is just what you want here.
A further note: I don't think that storing the number of sections in an instance variable is a good thing here. It makes for double bookkeeping.
I use enum for building my UITableView cells:
enum VAXSections: Int
{
case Segmented = 0
case Scrollable = 1
case ScheduledMode = 2
case ScheduledFooter = 3
case SilentHours = 4
case SilentFooter = 5
}
here how I use it:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
guard let unwrappedSection = VAXSections(rawValue: indexPath.section) else {
showFatalError()
return nil
}
Few problem here I want to guard my section value if it's out of max case in my enum. For example if indexPath.section will be bigger then 5 then app should fall back. But if you see we can't return nil here, as cellForRowAtIndexPath has to return cell in any case.
I can solve problem by providing more readability with replacing showFatalError() fiction with:
guard let unwrappedSection = VAXSections(rawValue: indexPath.section) else {
fatalError("Check \(String(VAXUnitDashboardViewController.self)) UITableView datasource or check \(String(VAXSections.self)) enum.")
}
then I don't need to return any value. But then I turned in another problem. As I need to specify at least 3 datasource functions of my UITableView I need to duplicate fatal error which I wish to replace with one function that do the same all the time:
fatalError("Check \(String(VAXUnitDashboardViewController.self)) UITableView datasource or check \(String(VAXSections.self)) enum.")
enum VAXItems: String {
case Item1
case Item2
case Item3
}
enum VAXSections: String {
case Segmented
case Scrollable
case ScheduledMode
case ScheduledFooter
case SilentHours
case SilentFooter
}
struct VAXModel {
var type: VAXSections
var items: [VAXItems]
}
Then on your UIViewController you can have:
class ViewController: UIViewController {
private var model: [VAXModel] = []
override func viewDidLoad() {
super.viewDidLoad()
model = [
VAXModel(type: .ScheduledMode, items: [.Item1, .Item2, .Item3]),
VAXModel(type: .SilentFooter, items: [.Item1])
]
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return model.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model[section].items.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(String(UITableViewCell), forIndexPath: indexPath)
let item = model[indexPath.section].items[indexPath.row]
switch item {
case .Item1: cell.textLabel?.text = item.rawValue
case .Item2: // Config
case .Item3: // Config
}
return cell
}
}
I don't think you need to have it in 3 places actually. Assuming that the 3 data source methods you are talking about are :
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
You actually need to have your guard in only one of them.
numberOfSectionsInTableView is the first method that will be called, so if you fail here, the other two methods won't be called. If the number of sections is based on some calculations, you can also cut off the value, something like this : if calculatedNumberOfSections > 6 { return 6 } else { return calculatedNumberOfSections } (remember that section numbering is 0 based)
numberOfRowsInSection - if you guard here you have two options - either fail with fatalError or (better in my opinion) - return 0 if a section number higher than 5 gets passed. Returning 0 will result in cellForRowAtIndexPath not being called with that section.
cellForRowAtIndexPath - you already got this :)
I am looking for a "good" way to solve some special requirements:
I have an UITableView with different sections, for example:
Base Data
About me
Interests
Images
Base Data contains always values (but there is still an variable row count) - and all other "Categories" could contain rows, or still could be empty. If there is no data, the category should be not shown.
No my first idea to solve that is:
Create all possible categories (but that could be 20 or more) - and do something like that:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var count:Int = 0
switch (section) {
case 0:
count = baseHeaders.count
case 1:
if(mapItem.courses?.count > 0) {
count = mapItem.courses!.count
}
break;
default:
count = 0
}
return count
}
And ill check also with: titleForHeaderInSection if the count is null, and return then no header for the section.
BUT: is that a good way? My concern is about creating 20 sections and just 2 are used. Is there another, better way? Can i create sections manually? If yes, should i? So that only the base category is visible, and append everything else if there is data available.
This looks like my way of approaching such problems. I'm using enums (Obj-C & especially Swift) to handle and identify my Sections and I always return the full amount of potential sections:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return FormSection.count // enum function
}
In func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int, however, I switch the unused sections off by returning 0 rows.
The benefit I saw after struggling with your type of dynamic tables was that all sections are always at the same index which made cell management relatively easy:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let section:FormSection = FormSection(rawValue:indexPath.section)!
switch section {
case .Header:
//…
default:
//…
}
}
The same goes for section headers/footers:
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
switch section {
case FormSection.Header.rawValue:
return nil
case FormSection.RoomSetup.rawValue where foo == false:
return nil
default:
// return header with title = FormSection(rawValue: section)?.headerTitle()
// Swift enums ftw ;)
}
And the number of rows is calculated/fetched at runtime:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section:FormSection = FormSection(rawValue:section)!
switch section {
case .Section1:
return fooExpanded ? (numberOfFoo) : 0
case .Section2:
return model.barCount()
default:
return 1
}
}
Here's what I'm trying to do:
UITableView in editing mode with 2 sections.
User can move cells from second section into first and vice versa.
In the first section user can reorder cells as he wants.
In the second section positioning of cells is fixed, so if user moves a cell from first section into second, it should move to a specific place, not where user want.
For implementing this behavior I have 2 arrays: one for first section and one for second section (not sure if it's the best choice).
Here's the code that controls where user should move a cell:
func tableView(tableView: UITableView, targetIndexPathForMoveFromRowAtIndexPath sourceIndexPath: NSIndexPath, toProposedIndexPath proposedDestinationIndexPath: NSIndexPath) -> NSIndexPath {
if proposedDestinationIndexPath.section == 1 {
let item = (sourceIndexPath.section == 0 ? firstSectionItems[sourceIndexPath.row] : secondSectionItems[sourceIndexPath.row]).item
return NSIndexPath(forRow: item.displayOrder.integerValue, inSection: 1)
}
return proposedDestinationIndexPath
}
Here's the code for moving items between sections:
func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
let section = (source: sourceIndexPath.section, destination: destinationIndexPath.section)
switch section {
case (0, 0):
let itemToMove = firstSectionItems.removeAtIndex(sourceIndexPath.row)
itemToMove.item.order = destinationIndexPath.row
firstSectionItems.insert(itemToMove, atIndex: destinationIndexPath.row)
case (1, 1):
let itemToMove = secondSectionItems.removeAtIndex(sourceIndexPath.row)
secondSectionItems.insert(itemToMove, atIndex: destinationIndexPath.row)
case (1, 0):
let itemToMove = secondSectionItems.removeAtIndex(sourceIndexPath.row)
itemToMove.item.order = destinationIndexPath.row
firstSectionItems.insert(itemToMove, atIndex: destinationIndexPath.row)
case (0, 1):
let itemToMove = firstSectionItems.removeAtIndex(sourceIndexPath.row)
itemToMove.item.order = -1
secondSectionItems.insert(itemToMove, atIndex: destinationIndexPath.row)
default:
break
}
DatabaseConnector.saveContext()
}
Problem is that if I have several items in the first section and I try to move last one into the second section, it crashes when I drop the cell on the second section and shows me EXC_BAD_ACCESS on App Delegate with no output to console.
Debug navigator doesn't tell me much, moveRowAtIndexPath: doesn't get called. The last method that gets called is -[UISectionRowData insertRowAtIndex:inSection:rowHeight:tableViewRowData:].
Sometimes I get a strange message in console when it crashes:
warning: could not load any Objective-C class information from the
dyld shared cache. This will significantly reduce the quality of type
information available.
I'm using Swift 2.
Well, the problem was in displayOrder of an item in tableView(_:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath:) method.
If you have, say, 5 elements in an array and the displayOrder of the one you want to insert is, say, 7, it'll crash because last indexPath.row in that section will be 5, but you are trying to insert with indexPath.row = 7, which is can't happen. You can insert with indexPath.row = 6 though, because it's the next after the last indexPath in the table view.
So this is how that method now looks like:
func tableView(tableView: UITableView, targetIndexPathForMoveFromRowAtIndexPath sourceIndexPath: NSIndexPath, toProposedIndexPath proposedDestinationIndexPath: NSIndexPath) -> NSIndexPath {
if proposedDestinationIndexPath.section == 1 {
let item = (sourceIndexPath.section == 0 ? firstSectionItems[sourceIndexPath.row] : secondSectionItems[sourceIndexPath.row]).item
let rowIndex = item.displayOrder > secondSectionItems.count ? secondSectionItems.count : item.displayOrder
return NSIndexPath(forRow: rowIndex, inSection: 1)
}
return proposedDestinationIndexPath
}
And it works just fine.
(stupid mistake :-/)