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.
Related
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)
}
}
The problem is quite simple.
I am attempting to animate the addition of a new row into a new section.
The update code:
func updateTableView(sessions: [Sessions]) {
self.foundSessions = sessions
if self.numberOfSections(in: self.tableView) == 0 {
self.tableView.beginUpdates()
self.tableView.insertSections([0], with: .automatic)
self.tableView.endUpdates()
}
for i in 0..<self.foundSessions.count {
self.tableView.beginUpdates()
self.tableView.insertRows(at: [IndexPath(row: i, section: 0)], with: .automatic
self.tableView.endUpdates()
}
}
UITableViewDataSource code:
override func numberOfSections(in tableView: UITableView) -> Int {
if foundSessions.isEmpty { return 0 }
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return foundSessions.count
}
And the error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert section 0 but there are only 0 sections after the update
Obviously, it seems the section is not getting inserted. If I take out insertRows, the empty section eventually shows up, as can be seen by the tableFooterView at the top. I have followed the advice of the posts here, here, here, and here, and have read the respective Apple Documentation on the manner, obviously to no avail.
Any iOS gurus out there able to show me the error in my ways?
EDIT: More debugging info - updateTableView is called in the callback of an async function. I wrapped all this code in a DispatchQueue.main.async block in an attempt to remedy the situation, however, was unsuccessful.
Try putting all your updates to section and it's row in one block, begin ... end like below
self.tableView.beginUpdates()
self.tableView.insertSections([0], with: .automatic)
for i in 0..<self.foundSessions.count {
self.tableView.insertRows(at: [IndexPath(row: i, section: 0)], with: .automatic
}
self.tableView.endUpdates()
I'm trying to use the iOS 11 way to add swipe actions in a table view row. I want to add an action to delete a row.
My test table view displays numbers from 0 to 9, the data source is a simple array of integers, called numbers:
class ViewController: UIViewController {
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}
When I select a row, I print the associated value of numbers:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(numbers[indexPath.row])
tableView.deselectRow(at: indexPath, animated: true)
}
I implement the new delegate trailingSwipeActionsConfigurationForRowAt as below:
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let delete = UIContextualAction(style: .destructive, title: "Delete") { (_, _, completionHandler) in
self.numbers.remove(at: indexPath.row)
completionHandler(true)
}
return UISwipeActionsConfiguration(actions: [delete])
}
When I swipe on a row, the deletion works fine. But, and that's my problem, when I select another row after the deletion, the associated IndexPath is wrong...
Example:
I select the first row, the value printed is 0: OK.
I delete the first row.
I select the new first row, the value printed is 2, because the IndexPath passed in parameter is row 1, instead of row 0: not OK.
What do I do the wrong way?
It is important to remember that there's a model, the numbers array in your case, and a view, the cells that are displayed. When you remove the element from numbers you are only updating the model.
In most cases, as you have it coded, you would get an error shortly thereafter because the model is out of sync with the view.
However, my testing indicates that when the style is .destructive and you pass true to completionHandler Apple is sort of removing the row for you. Which is why you are seeing the row disappear. But there's something not quite right about it and I can't find any documentation on it.
In the meantime, just do this:
self.numbers.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completionHandler(true)
And always remember that if you change with the model you need to update the view.
It is very confusing that calling the completion handler with true updates the view by removing the cell, but does not update the table view's notion of which which remaining cells are at which index path.
The documentation around this is very scant, so I ran a test and found that calling the completion handler with true FIRST then calling deleteRows results in the "most correct" animation.
Batch updates with completion called first:
tableView.beginUpdates()
completion(succeeded)
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
Batch updates with completion called second:
tableView.beginUpdates()
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(succeeded)
tableView.endUpdates()
Completion called first:
completion(succeeded)
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
Completion called second
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(succeeded)
Image mid-animation:
The right panels both have the remaining cell partially covered by the cell that is being removed, while the bottom left panel introduces a white view from somewhere.
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()