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

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

Related

How to move and reload UITableView row within same beginUpdates / endUpdates operation?

I am working on quite complex updates on a UITableView. Multiple rows can be moved, deleted, inserted and changed at the same time. What is the correct way to handle these changes within the same tableView.beginUpdates()/ tableView.endUpdates()block?
Example:
Data before update ==> Data after update
================== =================
0: zero 0: zero
1: one 1: 555 (updated element five)
2: two 2: three
3: three 3: 222 (updated element two)
4: four
5: five
==> Elements 1 and 4 are deleted
==> Elements 2 and 5 are updated (content changes) and they switch their position
This can be done using a simple ViewController:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
#IBOutlet var button: UIButton!
var rowTitles = ["zero", "one", "two", "three", "four", "five"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rowTitles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = rowTitles[indexPath.row]
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
#IBAction func click() {
tableView.beginUpdates()
// "zero", "one", "555", "three", "four", "222"
rowTitles[2] = "555"
rowTitles[5] = "222"
// "zero", "555", "three", "four", "222"
rowTitles.remove(at: 1)
// "zero", "555", "three", "222"
rowTitles.remove(at: 3)
// Delete "one" (original index = 1)
tableView.deleteRows(at: [IndexPath(row: 1, section: 0)], with: .automatic)
// Delete "four" (original index = 4)
tableView.deleteRows(at: [IndexPath(row: 4, section: 0)], with: .automatic)
// Move "two" and "five", use new old index for source and new index for destination
tableView.moveRow(at: IndexPath(row: 2, section: 0), to: IndexPath(row: 3, section: 0))
tableView.moveRow(at: IndexPath(row: 5, section: 0), to: IndexPath(row: 1, section: 0))
// CRASH
//tableView.reloadRows(at: [IndexPath(row: 5, section: 0)], with: .automatic)
tableView.endUpdates()
}
}
Everything works fine, except moving and reloading the rows 2 and 5 at the same time. I reloadRows(...) is not used, the code runs but results in a list zero, five, three, two. So, the elements in the correct order, but the rows have not been updated to 555 and 222.
When using reloadRows(...) with the original indexes 2 and 5 the app crashes because move+delete have been called for the same indexes (it seems that reload is internally handled as delete+add).
Using reloadRows(...) AFTER endUpdates() delivers the correct result. However, I wonder if this is the correct way to go. Regarding the Apple Docs reloadRows(...) should be called within beginUpdates ... endUpdates.
So, what is the correct way to solve this?
Not entirely sure what visual effect you're going for, but...
You probably want to reload the table view and then remove / rearrange the rows.
Try it like this:
#IBAction func click() {
rowTitles[5] = "555"
rowTitles[2] = "222"
let p1 = IndexPath(row: 2, section: 0)
let p2 = IndexPath(row: 5, section: 0)
self.tableView.reloadRows(at: [p1, p2], with: .automatic)
rowTitles.remove(at: 1)
rowTitles.remove(at: 3)
// need to update order in array
rowTitles[1] = "555"
rowTitles[3] = "222"
tableView.performBatchUpdates({
// Delete "one" (original index = 1)
tableView.deleteRows(at: [IndexPath(row: 1, section: 0)], with: .automatic)
// Delete "four" (original index = 4)
tableView.deleteRows(at: [IndexPath(row: 4, section: 0)], with: .automatic)
// Move "two" and "five", use new old index for source and new index for destination
tableView.moveRow(at: IndexPath(row: 2, section: 0), to: IndexPath(row: 3, section: 0))
tableView.moveRow(at: IndexPath(row: 5, section: 0), to: IndexPath(row: 1, section: 0))
}, completion: { _ in
// if you want to do something on completion...
})
}
End result order is:
zero
555
three
222

How to reload the last cell in a section in swift

I am trying to load a cell into a certain section of my tableView with an animation, but It is crashing giving me this error 'NSInternalInconsistencyException', reason: 'attempt to delete row 1 from section 0 which only contains 1 rows before the update'
from this code:
let indexPath = IndexPath(item: session[section].count - 1, section: section)
tableView.reloadRows(at: [indexPath], with: .automatic)
this code is triggered after a user clicks a button to add another cell to the end of that section. All I want to do is add that cell to the end of the section with an animation, how can I do that?
#objc func addSetPressed(_ sender: UIButton){
let section = sender.tag
session[section].append(ExerciseSet(setID: "", sessionID: "", exerciseID: "", setIndex: section + 1, weight: session[section][0].weight, reps: session[section][0].reps))
//tableView.reloadData()
let indexPath = IndexPath(item: session[section].count - 1, section: section)
tableView.reloadRows(at: [indexPath], with: .automatic)
setTableViewHeight()
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
}

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

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

Syncing UITableView with the Array

I have a UITableView which is shown when the screen is loaded. The UITableView shows 7 items since the dishes array contains 7 items. I have button which adds one more item to the dishes array.
// adding just one more item to the array
self.dishes.append(contentsOf: filteredArray)
let newIndexPath = IndexPath(row: self.dishes.count + 1, section: 0)
self.tableView.insertRows(at: [newIndexPath], with: .automatic)
As soon as the new dish is added I get the following error:
'NSInternalInconsistencyException', reason: 'attempt to insert row 10 into section 0, but there are only 9 rows in section 0 after the update'
*** First throw call stack:
I would like to see more of the code to understand what you're doing but I think you should be doing it more like this.
var dishes = [Dish]()
#IBAction func myButton(_ sender: Any) {
dishes.append(Dish())
collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dishes.count
}
The last row index will be self.dishes.count without adding 1.

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

Resources