Animating inserting a new row into a new section in a UITableView - ios

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()

Related

How to collapse tableview with animation?

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)
}
}

iOS Collapse/ Expand Sections by clicking another section in TableView

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.

tableView.insertRows throws NSException, but not when `insertRows` at `[[0,0]]`

I am following a modified (simplified) version of the tutorial very much like what is found here:
https://developer.apple.com/library/content/referencelibrary/GettingStarted/DevelopiOSAppsSwift/ImplementNavigation.html#//apple_ref/doc/uid/TP40015214-CH16-SW1
and here is my UITableViewController:
import UIKit
class NewsTableViewController: UITableViewController {
#IBOutlet var newsTableView: UITableView!
/*
MARK: properties
*/
var news = NewsData()
override func viewDidLoad() {
super.viewDidLoad()
dummyNewData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return news.length()
}
// here we communicate with parts of the app that owns the data
override func tableView(_ tableView: UITableView
, cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
// note here we're using the native cell class
let cell = tableView.dequeueReusableCell(withIdentifier: "newsCell", for: indexPath)
// Configure the cell...
let row : Int = indexPath.row
cell.textLabel?.text = news.read(idx: row)
return cell
}
// MARK: Navigation ****************************************************************
// accept message from CreateNewViewController
#IBAction func unwindToCreateNewView(sender: UIStoryboardSegue){
if let srcViewController = sender.source as? CreateNewsViewController
, let msg = srcViewController.message {
// push into news instance and display on table
news.write(msg: msg)
let idxPath = IndexPath(row: news.length(), section: 1)
// tableView.insertRows(at: [idxPath], with: .automatic)
tableView.insertRows(at: [[0,0]], with: .automatic)
print("unwound with message: ", msg, idxPath)
print("news now has n pieces of news: ", news.length())
print("the last news is: ", news.peek())
}
}
/*
#DEBUG: debugging functions that display things on screen **************************
*/
// push some values into new data
private func dummyNewData(){
print("dummyNewData")
news.write(msg: "hello world first message")
news.write(msg: "hello world second message")
news.write(msg: "hello world third message")
}
}
The problem is in the function unwindToCreateNewView:
let idxPath = IndexPath(row: news.length(), section: 1)
tableView.insertRows(at: [idxPath], with: .automatic)
where news.length() gives me an Int that is basically someArray.count.
When I insertRows(at: [idxPath] ...), I get error:
libc++abi.dylib: terminating with uncaught exception of type NSException
But when I just hard code it to do:
tableView.insertRows(at: [[0,0]], with: .automatic)
It works just fine. And on the simulator I see new messages are inserted below the previous ones. What gives?
You have an "off by one" problem with the following code:
news.write(msg: msg)
let idxPath = IndexPath(row: news.length(), section: 1)
Let's say that just before this code is called, you have no items in news. This means there are 0 rows. When you want to add a new row, you need to insert at row 0 since row numbers start at 0.
Calling news.write(msg: msg) add the new item and its length is now 1.
Calling IndexPath(row: news.length(), section: 1) sets the row to a value of 1 but it needs to be 0.
One simple solution is to swap those two lines:
let idxPath = IndexPath(row: news.length(), section: 1)
news.write(msg: msg)
That will create the index path with the proper row number.
And since this is the first (and only) section, the section number needs to be changed to 0 in addition to the above change.
let idxPath = IndexPath(row: news.length(), section: 0)
news.write(msg: msg)

Deleting cell leads to a crash

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()

Add sections and rows to tableView during search, as in Tweetbot

I'm trying to create a search view controller quite like Tweetbot's, where adding text to the search bar inserts a new section and new rows into the tableView,
like so.
I've tried using the searchBar delegate methods searchBarTextDidBeginEditing and
searchBar(_:, textDidChange:) but my attempts to insert a new section and rows within the method resulted in crashes.
What I tried:
tableView.beginUpdates()
tableView.insertSections(NSIndexSet(index: 0), withRowAnimation: .None)
tableView.insertRowsAtIndexPaths(
[
NSIndexPath(forRow: 0, inSection: 0),
NSIndexPath(forRow: 1, inSection: 0),
NSIndexPath(forRow: 2, inSection: 0)
],
withRowAnimation: .None)
tableView.endUpdates()
The error I got:
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 (3) 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
(1 inserted, 0 deleted).'
*** First throw call stack:
(0x181a19900 0x181087f80 0x181a197d0 0x18238c99c 0x18695c724 0x10008b578
0x10008b5e8 0x18696d0a0 0x18679bc48 0x18679bbc4 0x186783880 0x186782bfc
0x18677ef98 0x186875268 0x18696cdfc 0x18696fe60 0x1869c817c 0x18696d02c
0x1867eca24 0x10029004c 0x1867ecea4 0x186873d38 0x186924b84 0x186924038
0x186ce2a18 0x186909158 0x1867967a8 0x186ce4018 0x186755960 0x1867526e4
0x186794618 0x186793c14 0x1867642c4 0x18676258c 0x1819d0efc 0x1819d0990
0x1819ce690 0x1818fd680 0x182e0c088 0x1867cd40c 0x100123594 0x18149e8b8)
libc++abi.dylib: terminating with uncaught exception of type NSException
Thanks for your help.
After you call tableView.endUpdates() your table view will call its UITableViewDataSource delegate and it expects that tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int method to return 3. (because you added 3 rows in the first section).
Try something like this:
Declare new property:
var showSearchSelection = false
Search:
tableView.beginUpdates()
tableView.insertSections(NSIndexSet(index: 0), withRowAnimation: .None)
tableView.insertRowsAtIndexPaths(
[
NSIndexPath(forRow: 0, inSection: 0),
NSIndexPath(forRow: 1, inSection: 0),
NSIndexPath(forRow: 2, inSection: 0)
],
withRowAnimation: .None)
showSearchSelection = true
tableView.endUpdates()
UITableViewDataSource delegate:
extension UIViewController : UITableViewDataSource {
public func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
public func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section:
case 0:
return showSearchSelection ? 3 : 0
default:
return 0
}
}

Resources