I have a tableview with two sections.
var categories = ["a", "b", "c"]
var items = ["1" , "2", "3"]//will be different based on category selection
var selectedCategory: String?
Initially first section only visible. After the user selects any row in 0th section section 1 will have 3 rows and section 0 will have only selected row.
For ex. if category "b" is selected "a","c" rows should be removed and "1","2","3" rows should be added.
Now if the "b" is deselected "a","c" rows should be added and "1","2","3" rows should be removed.
//UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
return selectedCategory == nil ? 1 : 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 0 ? categories.count : items.count
}
I've done this using tableView.reloadData(). It does't show any animation. I'm trying to achieve this using the following methods
tableView.deleteRows(at: [IndexPath], with: UITableViewRowAnimation)
tableView.insertRows(at: [IndexPath], with: UITableViewRowAnimation)
I'm struggling to get indexpaths to insert and delete
instead of reload whole tableView do this:-
tableView.beginUpdates()
tableView.deleteRows(at: [IndexPath], with: UITableViewRowAnimation.top)
tableView.insertRows(at: [IndexPath], with: UITableViewRowAnimation.top)
tableView.endUpdates()
You need to wrap the deleteRows and insertRows call in tableView.beginUpdates and tableView.endUpdates.
https://developer.apple.com/documentation/uikit/uitableview/1614908-beginupdates?language=objc
Or use performBatchUpdates:completion:.
https://developer.apple.com/documentation/uikit/uitableview/2887515-performbatchupdates?language=objc
Edit
Ok, let me explain some things in detail. :-)
First I think you need to modify numberOfRowsInSection, because you said section 0 should show all categories as long as none is selected and after selecting one, it should display only that one row.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
// If no category is selected, we display all categories, otherwise only the selected one.
return selectedCategory == nil ? categories.count : 1
case 1:
return items.count
default:
return 0
}
}
Second, this is an example of how didSelectRowAt could look like. I changed selectedCategory to Int? to store the index of the selected category and not its name.
As you can see, in section 0 the rows that are not the selected category are deleted, while section 1 is added completely, as it did not exist before.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if selectedCategory == nil {
// Save index on selected category
selectedCategory = indexPath.row
// set items array
…
// Animate change
var indexPaths = [IndexPath]()
for (index, _) in categories.enumerated() {
if index != selectedCategory {
indexPaths.append(IndexPath(row: index, section: 0))
}
}
tableView.performBatchUpdates({
// delete rows from section 0
tableView.deleteRows(at: indexPaths, with: .automatic)
// insert section 1
tableView.insertSections(IndexSet(integer: 1), with: .automatic)
}, completion: nil)
}
}
Related
I have a question about making a table view section expand/collapse. There are many pieces of information on the web about this topic, but my question is a little bit different.
My question is, how can I make an additional section when the user taps on another section?
The following images may help you understand my situation.
At the top of the table view we have one small section (section 0).
If I tap it...
Section 1 and 2 should be created between section 0 and 3.
How can I do this? Is there any way or code? Please help me.
Also, I tried this code but got a sigabrt error :(
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView.beginUpdates()
let indexSet = NSMutableIndexSet()
indexSet.add(indexPath.section + 1)
indexSet.add(indexPath.section + 2)
if indexPath.section == 0 {
if expandCol == true {
self.tableView.deleteSections(NSIndexSet(index: 1) as IndexSet, with: .automatic)
self.tableView.deleteSections(NSIndexSet(index: 2) as IndexSet, with: .automatic)
expandCol = !expandCol
} else {
self.tableView.insertSections(NSIndexSet(index: 1) as IndexSet, with: .automatic)
self.tableView.insertSections(NSIndexSet(index: 2) as IndexSet, with: .automatic)
expandCol = !expandCol
}
}
self.tableView.endUpdates()
self.tableView.reloadData()
}
You can keep a flag variable on view-controller for selectedSection.
var selectedSection: Int = -1
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == selectedSection ? <section-row-count> : 0
}
Set the flag value in section header action method:
func actionSectionHeader(tag: Int) {
selectedSection = tag
self.tableView.reloadData()
}
To add a new section into UITableView, you need
1 - Update your data. For example, insert your sections in a data array, or change their visibility flags.
2 - Call the following method.
tableView.insertSections([1, 2], with: .automatic)
or
tableView.reloadData()
The first method will update your UITableView with an animation. The second one will update it instantly. For better UX I'd recommend you to use the first one.
Update on your code
At first, you can pass all sections indices at once using a simple array. Like that
if expandCol {
self.tableView.deleteSections([1, 2], with: .automatic)
} else {
self.tableView.insertSections([1, 2], with: .automatic)
}
expandCol = !expandCol
The second problem is that you're calling reloadData after you call the animated update of your table.
And the most important, you need to change the data. You UITableView has a dataSource and it returns a number of sections and cells. After user interacts with your trigger cell, you need to make changes at that data before calling the update.
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
I have a tableview with 2 sections and multiple rows in each. I'm trying to remove a row when a button confirmation is tapped.
Orgininal data looks like this
{
"section one": [
{},
{}
],
"section two": [
{},
{},
{}
]
}
I create an array of sections where each one includes an array of objects. Everything displays correctly just can't get the delete.
In my service:
var items = [Items]()
var mySections = [MySection]()
In my view:
func numberOfSections(in tableView: UITableView) -> Int {
return myService.mySections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let mySection = myService.mySections[section]
return mySection.items.count
}
I've been playing around with this as lots of examples show:
myService.mySections[indexPath.section].remove(at: indexPath.row)
let indexPath = IndexPath(item: 0, section: 0)
followupTable.deleteRows(at: [indexPath], with: .fade)
I cannot seem to target it correctly, I can't simply use the .remove because of the nested data. The remove error is Value of type 'MySection' has no member 'remove'
Given the data structures shown in numberOfRowsInSection, your code to remove a row needs to be like the following:
myService.mySections[indexPath.section].items.remove(at: indexPath.row)
followupTable.deleteRows(at: [indexPath], with: .fade)
But this does require that items be a var and not a let. So you may need to update your data structure to support this.
Also note that you do not want to delete an item from some specific index path and then remove the cell at index path 0, 0. You want to remove from the same index path.
To get your issue to work you must have an array of arrays like this
var mySections = [["A","B","C"],["D","E","F"]]
here mySections is [[String]] , you can have [[MySection]]
Then you will be able to
mySections[indexPath.section].remove(at: indexPath.row)
I am working on expanding and collapsing table view cell. The current situation works well , where assume Row-0 is tapped a new row is added at index-1 and shown as expanded and when again Row-0 is tapped the expanded index-1 is collapsed. If Row-0 is in expanded state and even if I tap on another Row, the row will expand keeping both open at the same time.
Here is what I want to change, I want to contract any other row which is open when I tap on another row. i.e. One row should be open at a time. Please see the below code for ref which I tried using an array to detect which row is currently expanded, this works only if we go sequentially else it starts deleting the contracted rows. Any help will be highly appreciated.
Thank You !
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
if expandCellArray.object(at: indexPath.row) as! String == "contract"
{
for i in 0 ..< self.expandCellArray.count
{
if expandCellArray.object(at: i) as! String == "expanded"
{
self.contractCell(tableView: tableView, index: i)
self.expandCellArray.replaceObject(at: i, with: "contract")
print("Updated Expand Cell array",expandCellArray)
}
}
self.expandCell(tableView: tableView, index: indexPath.row)
self.expandCellArray.replaceObject(at: indexPath.row, with: "expanded")
print("Updated Expand Cell array",expandCellArray)
}
else
{
self.contractCell(tableView: tableView, index: self.selectedIndexPath.row)
}
}
Function to Expand Row
private func expandCell(tableView: UITableView, index: Int)
{
if (dataArray[index]?.title) != nil {
dataArray.insert(nil, at: index + 1)
myTableView.insertRows(at: [IndexPath(row: index + 1, section: 0)], with: .none)
}
}
Function to Contract row
private func contractCell(tableView: UITableView, index: Int)
{
if (dataArray[index]?.title) != nil {
dataArray.remove(at: index + 1)
myTableView.deleteRows(at: [IndexPath(row: index + 1, section: 0 )], with: .none)
}
}
Adding or removing cells is tricky because you have to handle the indexes. Alternatively, a simpler implementation of this is to use expandable sections.
Instead of adding and deleting, you just simply showing and hiding them with animation. If you want to keep only one section open, when user click on another section, simply collapse the opened section and do section reload on both sections(The one that user clicked and the one that is currently open)
So my numberOfRowsInSection is:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let delegate = delegate else { return 0 }
return delegate?.comments.count + 1
}
I added one more to compensate for the "load more" which is on top of the table view (in this case, it's row 0).
Here's the thing: when I want to remove that cell "load more" after fetching the max amount of posts, it gives me an error:
The number of rows contained in an existing section after the update (12) must be equal to the number of rows contained in that section before the update (11), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
So here's the function that causes the error:
func loadMore() {
// loadMoreComments(completion:) inserts the new comment objects inside
// the data source in the beginning of the list.
delegate?.loadMoreComments(completion: { (isEndOfPosts, newCommentCount, comments) in
self.didReachEndOfPosts = isEndOfPosts
if isEndOfPosts {
// indexPaths just returns [[0,1]], or, just one row.
let indexPaths = self.createIndexPathsForNoMorePosts(newCommentCount: newCommentCount)
self.tableView.beginUpdates()
// error happens here.
self.tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
self.tableView.endUpdates()
// assume only one post comes in, and that's the last one.
self.tableView.beginUpdates()
self.tableView.insertRows(at: indexPaths, with: .automatic)
self.tableView.endUpdates()
}
}
What I'm trying to accomplish is this: once i get the last post, remove the "load more" cell, and insert the last few posts, replacing the 0 row with the first of the last few posts.
You need to notify your tableView's datasource about cell count change. Create a class variable, something like
var shouldShowLoadMoreCell = true
and than modify numberOfRowsInSection method
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let delegate = delegate else { return 0 }
if shouldShowLoadMoreCell {
return delegate?.comments.count + 1
} else {
return delegate?.comments.count
}
}
finally, set this flag when needed
self.tableView.beginUpdates()
shouldShowLoadMoreCell = false
self.tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
self.tableView.endUpdates()