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
Related
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()
}
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)
}
}
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)
Preparation work:
Create a new single view project
embed the view controller in a navigation controller
drag two bar button items onto the navigation bar
drag a table view onto the root view, make view controller the data source of the table view
My code is as below:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var sec = [["00", "01", "02"],
["10", "11", "12"]]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension ViewController: UITableViewDataSource {
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sec.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sec[section].count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = sec[indexPath.section][indexPath.row]
return cell
}
}
extension ViewController {
#IBAction func deleteUpSection(sender: UIBarButtonItem) {
sec[1][0] = "something else"
sec.removeAtIndex(0)
let deletedIndexPaths = [NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 1, inSection: 0),NSIndexPath(forRow: 2, inSection: 0)]
let deletedIndexSet = NSIndexSet(index: 0)
let reloadedIndexPaths = [NSIndexPath(forRow: 0, inSection: 1)]
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths(deletedIndexPaths, withRowAnimation: .Fade)
tableView.deleteSections(deletedIndexSet, withRowAnimation: .Right)
tableView.reloadRowsAtIndexPaths(reloadedIndexPaths, withRowAnimation: .Automatic)
tableView.endUpdates()
}
#IBAction func deleteDownSection(sender: UIBarButtonItem) {
sec[0][0] = "something else"
sec.removeAtIndex(1)
let deletedIndexPaths = [NSIndexPath(forRow: 0, inSection: 1), NSIndexPath(forRow: 1, inSection: 1), NSIndexPath(forRow: 2, inSection: 1)]
let deletedIndexSet = NSIndexSet(index: 1)
let reloadedIndexPaths = [NSIndexPath(forRow: 0, inSection: 0)]
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths(deletedIndexPaths, withRowAnimation: .Fade)
tableView.deleteSections(deletedIndexSet, withRowAnimation: .Right)
tableView.reloadRowsAtIndexPaths(reloadedIndexPaths, withRowAnimation: .Automatic)
tableView.endUpdates()
}
}
deleteDownSection works as expected, but deleteUpSection crashed, they are nearly the same.
Something I have discovered:
If I remove deleteRowsAtIndexPath in deleteUpSection, it works as expected, both deletion and update are executed.
If I remove reloadRowsAtIndexPath in deleteUpSection, it deletes the upper section successfully.
Any opinion is welcome.
reloadRowsAtIndexPaths is failing because you are attempting to reload rows in a section that no longer exists (NSIndexPath(forRow: 0, inSection: 1)). You deleted the rows of section 0 and section 0 itself in the prior statements which means there is only one section left (ie. the old section 1 which is now section 0).
I'm guessing when you remove deleteRowsAtIndexPath and it works it is because even though you remove the section in the next statement, those rows are still temporarily referenced somewhere so when reloadRowsAtIndexPaths is called it does not fail to find those rows even though its section is deleted.
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
}
}