A strange behaviour of table view batch operation - ios

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.

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

insert row at index path call scroll to top table view?

When i insert row at index path in uitableview, then my tableview scroll to top? Why?
let indexPathForCell = NSIndexPath(forRow: 5, inSection: 1)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathForCell], withRowAnimation: .Automatic)
tableView.endUpdates()
All code that is invoked during the addition of the cell
func buttonDidPressed(button: CheckMarkView) {
let indexPathForCell = NSIndexPath(forRow: 5, inSection: 1)
buttonPressedTag = button.tag
for checkMark in buttons {
if checkMark.tag == buttonPressedTag {
if buttonPressedTag == 4 {
checkMark.show()
checkMark.userInteractionEnabled = false
cellWithCategories["Recomendation"]?.append("slideCell")
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathForCell], withRowAnimation: .None)
tableView.endUpdates()
}
checkMark.show()
} else {
if (tableView.cellForRowAtIndexPath(indexPathForCell) != nil) {
cellWithCategories["Recomendation"]?.removeLast()
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([indexPathForCell], withRowAnimation: .None)
tableView.endUpdates()
}
checkMark.hide()
checkMark.userInteractionEnabled = true
}
}
}
code for number of rows :
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionKey = keysForSectionTableView[section]
let numberOfRows = cellWithCategories[sectionKey]
return (numberOfRows?.count)!
}
I don't see any code that will make your table view scroll to top.
But you can try change animation to none. If doesn't work then there is must be some other code, thats causing this issue.
let indexPathForCell = NSIndexPath(forRow: 5, inSection: 1)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathForCell], withRowAnimation: .None)
tableView.endUpdates()

Updating data source after animating cell removal from UITableView

I have a button in my custom cell that deletes the cell.
So i have a delegate that removes it.
code in view controller:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("swipeTableViewCell", forIndexPath: indexPath) as! swipeTableViewCell
cell.initCell(self, indexPath: indexPath, text: data[indexPath.row])
return cell
}
delegate method:
func removeCell(indexPath: NSIndexPath){
data.removeAtIndex(indexPath.row)
table.beginUpdates()
table.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
table.endUpdates()
}
code in cell:
func initCell(handler: handleCells, indexPath: NSIndexPath, text: String) {
self.handler = handler
self.indexPath = indexPath
}
button pressed:
#IBAction func OnDelButtonClickListener(sender: UIButton) {
self.handler.removeCell(indexPath)
}
This removes the cell with animation but the reloadData is not called and then the cells have the wrong indexPath.
So when I press a second cells delete the wrong cell gets removed.
If I call reloadData after table.endUpdates() there is no animation.
if I call
let indexSet = NSIndexSet(index: indexPath.section)
self.table.reloadSections(indexSet, withRowAnimation: UITableViewRowAnimation.Automatic)
instead of
table.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
I don't get a removal animation.
Any suggestions?
Thanks
Have a look at Apple's programming guide for UITableViews, at the row deleting section.
I may be missing something in your code, but it looks like you don't actually delete the object in the datasource that corresponds to your deleted cell. Try removing the object from your datasource in the removeCell function before you delete the row.
func removeCell(indexPath: NSIndexPath){
// here you delete the object form the datasource
// after that, you do this
table.beginUpdates()
table.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
table.endUpdates()
}
I think the key problem is in the Cell indexPath could not update when the table view delete the cell.
so we can try create a help array in the ViewController,help us update the really data to delate.
lazy var listHelper:Array<Int> = {
var array = [Int]()
for i in 0...self.data.count {
array.append(i)
}
return array
}()
update the removeCell function to this :
func removeCell(indexPath: NSIndexPath) {
// if first delete delete the date, and remove index in help list
if indexPath.row < listHelper.count - 1 && indexPath.row == listHelper[indexPath.row] {
data.removeAtIndex(indexPath.row)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
listHelper.removeAtIndex(indexPath.row)
}else {
// if indexPath.row != listHelper[indexPath.row],we find the really data we want to delete, used Array extension .indexOf
let locationData = listHelper.indexOf(indexPath.row)
data.removeAtIndex(locationData!)
// we create NSIndexPath and delete it.
let theindexPath = NSIndexPath(forRow: locationData!, inSection: 0)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([theindexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
listHelper.removeAtIndex(locationData!)
}
}
the Extension of Array :
extension Array {
func indexOf <U: Equatable> (item: U) -> Int? {
if item is Element {
return Swift.find(unsafeBitCast(self, [U].self), item)
}
return nil
}
}
My English is poor. and U can see the code . I had try it, and it can work. I hope can solve you problem.

Moving UITableViewCell between Two Section ios Swift

I am using two section like,
Section 1 with
cell 0
cell 1
cell 3 and
Section 2 with
cell 0
cell 1
cell 3
but i want to move cell 0 of section 2 in section 1
can any one explain me with code using swift programming
Basically, you need to understand that the data source needs to be updated before the cells get swapped otherwise you have a crash.
Look at the following example:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var data : [[String]] = [["Mike", "John", "Jane"], ["Phil", "Tania", "Monica"]]
#IBOutlet weak var tableView: UITableView!
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return data.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data[section].count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as UITableViewCell
let name = data[indexPath.section][indexPath.row]
(cell.viewWithTag(22) as UILabel).text = "(" + String(indexPath.section) + ":" + String(indexPath.item) + ") " + name
return cell
}
#IBAction func pressed(sender: UIButton) {
// arbitrarily define two indexPaths for testing purposes
let fromIndexPath = NSIndexPath(forRow: 0, inSection: 0)
let toIndexPath = NSIndexPath(forRow: 0, inSection: 1)
// swap the data between the 2 (internal) arrays
let dataPiece = data[fromIndexPath.section][fromIndexPath.row]
data[toIndexPath.section].insert(dataPiece, atIndex: toIndexPath.row)
data[fromIndexPath.section].removeAtIndex(fromIndexPath.row)
// Do the move between the table view rows
self.tableView.moveRowAtIndexPath(fromIndexPath, toIndexPath: toIndexPath)
}
}
Here I have the simplest case of a 2-d Array which holds some names. I define it as [[String]] to avoid casting it later. Before the In my Storyboard I have a button which calls 'pressed'. I swap the data source and then call moveRowAtIndexPath.
Try to use:
tableView.moveRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 2), toIndexPath: NSIndexPath(forRow: 0, inSection: 1))
Just don't forget to update your data model before calling this method.

Invalid Update When adding Rows to UITableView

Invalid update: invalid number of rows in section 0. The number of
rows contained in an existing section after the update (5) must be
equal to the number of rows contained in that section before the
update (1), plus or minus the number of rows inserted or deleted from
that section (1 inserted, 0 deleted) and plus or minus the number of
rows moved into or out of that section (0 moved in, 0 moved out).
I'm trying to add rows to a table view when a user taps a row, to create an expandable section, however the extra rows aren't being counted before Xcode tries to add them in and as such causes this error (I think). Can anybody point me in the right direction?
// sectionExpanded is set to false in viewDidLoad. It is set to true when
// the user taps on the expandable section (section 0 in this case)
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 && sectionExpanded {
return 5
} else {
return 1
}
}
// This should recount the rows, add the new ones to a temporary array and then add
// them to the table causing the section to 'expand'.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedItem = menu[indexPath.row]
let cell = tableView.cellForRowAtIndexPath(indexPath) as MenuCell
if indexPath.section == 0 {
var rows: Int
var tmpArray: NSMutableArray = NSMutableArray()
sectionExpanded = !sectionExpanded
rows = tableView.numberOfRowsInSection(0)
for i in 1...rows {
var tmpIndexPath: NSIndexPath
tmpIndexPath = NSIndexPath(forRow: i, inSection: 0)
tmpArray.addObject(tmpIndexPath)
}
if !sectionExpanded {
tableView.deleteRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
} else {
tableView.insertRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
}
} else {
delegate?.rightItemSelected(selectedItem)
}
}
It is telling you that you are trying to insert 1 new row, but numberofrows should be 5, before was 1 and you are trying to insert 1 new row, thats 2. Theres your problem.
rows = tableView.numberOfRowsInSection(0) //this returns 1
for i in 1...rows { //
var tmpIndexPath: NSIndexPath
tmpIndexPath = NSIndexPath(forRow: i, inSection: 0)
tmpArray.addObject(tmpIndexPath)//this will contain only 1 object, because the loop will run only for 1 cycle
}
EDIT
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedItem = menu[indexPath.row]
let cell = tableView.cellForRowAtIndexPath(indexPath) as MenuCell
if indexPath.section == 0 {
var rows: Int
var tmpArray: NSMutableArray = NSMutableArray()
sectionExpanded = !sectionExpanded
rows = 1
if sectionExpanded {
rows = 5
}
for i in 1...rows {
var tmpIndexPath: NSIndexPath
tmpIndexPath = NSIndexPath(forRow: i, inSection: 0)
tmpArray.addObject(tmpIndexPath)
}
if !sectionExpanded {
tableView.deleteRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
} else {
tableView.insertRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
}
} else {
delegate?.rightItemSelected(selectedItem)
}
}
Since you know number of rows will be always 5 or 1, you can try something like this. However, this is not a standard approach, I would suggest to alter your datasource array.
Here is some example how to do it: http://www.nsprogrammer.com/2013/07/updating-uitableview-with-dynamic-data.html its for Objective-C but you will get the gist of it.
You can try modifying the data source and then reload the table.
You should use insertRowsAtIndexPaths... and the like between a beginUpdates() and endUpdates(). The tableView will collect all the changes after beginUpdates() and then will apply them coherently after endUpdates(). So try something like:
tableView.beginUpdates()
if !sectionExpanded {
tableView.deleteRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
} else {
tableView.insertRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
}
tableView.endUpdates()
Remember that after the call to endUpdates() the number of sections and rows must be consistent with your model.
Since I don't know about your model, here's a simple example:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var sectionExpanded: Bool = false {
didSet {
if oldValue != sectionExpanded {
let expIndexes = map(0..<model.count) { r in
NSIndexPath(forRow: r, inSection: 0)
}
// Here we start the updates
tableView.beginUpdates()
switch sectionExpanded {
case false:
// Collapsing
tableView.deleteRowsAtIndexPaths(expIndexes, withRowAnimation: .Top)
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Top)
default:
// Expanding
tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Top)
tableView.insertRowsAtIndexPaths(expIndexes, withRowAnimation: .Bottom)
}
// Updates ended
tableView.endUpdates()
}
}
}
let model = ["foo", "bar", "zoo"]
//MARK: UITableView DataSource
struct TableConstants {
static let sectionCellIdentifier = "SectionCell"
static let expandedCellIdentifier = "ExpandedCell"
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sectionExpanded ? model.count : 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
switch sectionExpanded {
case false:
let cell = tableView.dequeueReusableCellWithIdentifier(
TableConstants.sectionCellIdentifier,
forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = "The Section Collapsed Cell"
return cell
default:
let cell = tableView.dequeueReusableCellWithIdentifier(
TableConstants.expandedCellIdentifier,
forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = "\(model[indexPath.row])"
cell.detailTextLabel?.text = "Index: \(indexPath.row)"
return cell
}
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
sectionExpanded = !sectionExpanded
}
}
Note that I moved the table updates to the sectionExpanded observer.
You already have 1 row in section = 0, and trying to insert 5 new rows. You can only add 4 rows more to map with numberOfRowsInsection.
Try following code:
sectionExpanded = !sectionExpanded
rows = self.numberOfRowsInSection(0)-1
for i in 1...rows {
var tmpIndexPath: NSIndexPath
tmpIndexPath = NSIndexPath(forRow: i, inSection: 0)
tmpArray.addObject(tmpIndexPath)
}

Resources