Im creating Shopping List app. I use 2 sections for separating bought items and Items that are not bought. I use moveRow method for moving rows between said 2 sections. This is the code for moving rows.
if indexPath.section == 0 {
self.shoppingItems.remove(at: indexPath.row)
self.shoppingItemsBought.append(item)
self.tableView.beginUpdates()
let fromIndexPath = NSIndexPath(row: indexPath.row, section: 0)
let toIndexPath = NSIndexPath(row: 0, section: 1)
self.tableView.moveRow(at: fromIndexPath as IndexPath, to: toIndexPath as IndexPath)
self.tableView.endUpdates()
} else {
self.shoppingItemsBought.remove(at: indexPath.row)
self.shoppingItems.append(item)
self.tableView.beginUpdates()
let fromIndexPath = NSIndexPath(row: indexPath.row, section: 1)
let toIndexPath = NSIndexPath(row: 0, section: 0)
self.tableView.moveRow(at: fromIndexPath as IndexPath, to: toIndexPath as IndexPath)
self.tableView.endUpdates()
}
The rows change their location but without animation. What am I missing?
One option is to use CATransaction around the moveRow. Put your changes inside the CATransaction block and on completion, force the reload. This will do the reload with animations.
Example:
CATransaction.begin()
CATransaction.setCompletionBlock {
tableView.reloadData()
}
tableView.beginUpdates()
// Update the datasource
if indexPath.section == 0 {
self.shoppingItems.remove(at: indexPath.row)
self.shoppingItemsBought.append(item)
}
else {
self.shoppingItemsBought.remove(at: indexPath.row)
self.shoppingItems.append(item)
}
// Get new IndexPath (indexPath is the old one, as in current)
let newIndexPath = IndexPath(row: 0, section: indexPath.section == 0 ? 1 : 0)
// Move the row in tableview
tableView.moveRow(at: indexPath, to: newIndexPath)
tableView.endUpdates()
CATransaction.commit()
Related
I'm currently deleting and inserting rows based on the content within a class which is called a content builder. An example of this can be seen below.
func stateDidChange(id: String, _ state: Bool) {
switch id {
case ConfigureExerciseContentBuilderKeys.accessory.rawValue:
exerciseProgramming.isAccessory = state
if state {
if let customIncrementsItemIndex = self.configureExerciseContentBuilder.getContent().firstIndex(where: { ($0 as? FormLabelTextFieldItem)?.id == ConfigureExerciseContentBuilderKeys.increment.rawValue }),
let syncedWorkoutsItemIndex = self.configureExerciseContentBuilder.getContent().firstIndex(where: { ($0 as? ExerciseSyncedWorkouts)?.id == ConfigureExerciseContentBuilderKeys.syncedWorkouts.rawValue }) {
self.configureExerciseContentBuilder = ConfigureExerciseContentBuilder(type: type, exerciseProgramming: exerciseProgramming)
let incrementsIndexPath = IndexPath(item: customIncrementsItemIndex, section: 0)
let syncedIndexPath = IndexPath(item: syncedWorkoutsItemIndex, section: 0)
let spacerIndexPath = IndexPath(item: syncedWorkoutsItemIndex + 1, section: 0)
self.tableView.beginUpdates()
self.tableView.deleteRows(at: [incrementsIndexPath, syncedIndexPath, spacerIndexPath], with: .automatic)
self.tableView.endUpdates()
}
} else {
self.configureExerciseContentBuilder = ConfigureExerciseContentBuilder(type: type, exerciseProgramming: exerciseProgramming)
if let customIncrementsItemIndex = self.configureExerciseContentBuilder.getContent().firstIndex(where: { ($0 as? FormLabelTextFieldItem)?.id == ConfigureExerciseContentBuilderKeys.increment.rawValue }),
let syncedWorkoutsItemIndex = self.configureExerciseContentBuilder.getContent().firstIndex(where: { ($0 as? ExerciseSyncedWorkouts)?.id == ConfigureExerciseContentBuilderKeys.syncedWorkouts.rawValue }) {
let incrementsIndexPath = IndexPath(item: customIncrementsItemIndex, section: 0)
let syncedIndexPath = IndexPath(item: syncedWorkoutsItemIndex, section: 0)
let spacerIndexPath = IndexPath(item: syncedWorkoutsItemIndex + 1, section: 0)
self.tableView.beginUpdates()
self.tableView.insertRows(at: [incrementsIndexPath, syncedIndexPath, spacerIndexPath], with: .automatic) // Insert into the index of where the accessory row was/is
self.tableView.endUpdates()
}
}
default:
break
}
}
The rows are successfully deleted and inserted based on the logic above and I used the automatic parameter so that the tableview can handle this in the best way.
It's also worth noting that within the tableviewcell autolayout is used on elements within them and also I've configured the row height within the tableview to be dynamic based on its contents.
But for some reason I seem to be getting a weird choppy animation when I delete or insert rows for the green box that you can see in the video attached. Does anyone have any pointers/ideas as to why this may be happening?
Link to video
My app crashed due to 'attempt to delete item 1 from section 1, but there are only 1 sections before the update'
I found other articles said that the data source must keep in sync with the collectionView or tableView, so I remove the object from my array: alarms before I delete the item in my collectionView, this works on didSelectItemAtIndexPath. But I don't know why this doesn't work.
I also tried to print the array.count, it showed the object did remove form the array.
func disableAlarmAfterReceiving(notification: UILocalNotification) {
for alarm in alarms {
if alarm == notification.fireDate {
if let index = alarms.index(of: alarm) {
let indexPath = NSIndexPath(item: index, section: 1)
alarms.remove(at: index)
collectionView.deleteItems(at: [indexPath as IndexPath])
reloadCell()
}
}
}
}
func reloadCell() {
userDefaults.set(alarms, forKey: "alarms")
DispatchQueue.main.async {
self.collectionView.reloadData()
print("Cell reloaded")
print(self.alarms.count)
}
}
You are having single section so you need to delete it from the 0 section. Also use Swift 3 native IndexPath directly instead of NSIndexPath.
let indexPath = IndexPath(item: index, section: 0)
alarms.remove(at: index)
DispatchQueue.main.async {
collectionView.deleteItems(at: [indexPath])
}
Edit: Try to break the loop after deleting items from collectionView.
if let index = alarms.index(of: alarm) {
let indexPath = IndexPath(item: index, section: 0)
alarms.remove(at: index)
DispatchQueue.main.async {
collectionView.deleteItems(at: [indexPath])
}
reloadCell()
break
}
I'm using Cristik's answer from this post: Drop-Down List in UITableView in iOS
The code works fine, but I am trying to create logic so when the user clicks on a closed cell, every other open cell closes. So if the Account cell was open, when the user clicks on the Event one, the event one opens and the Account one closes.
I tried the following logic:
for (var x = 0; x < displayedRows.count; x++) {
let view = displayedRows[x]
if (view.isCollapsed == false && x != indexPath.row){
let range = x+1...x+view.children.count
displayedRows.removeRange(range)
let indexPaths = range.map{return NSIndexPath(forRow: $0, inSection: indexPath.section)}
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
}
}
Here is the full function:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: false)
let viewModel = displayedRows[indexPath.row]
if viewModel.children.count > 0 {
let range = indexPath.row+1...indexPath.row+viewModel.children.count
let indexPaths = range.map{return NSIndexPath(forRow: $0, inSection: indexPath.section)}
tableView.beginUpdates()
if viewModel.isCollapsed {
displayedRows.insertContentsOf(viewModel.children, at: indexPath.row+1)
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
for (var x = 0; x < displayedRows.count; x++) {
let view = displayedRows[x]
if (view.isCollapsed == false && x != indexPath.row){
let range = x+1...x+view.children.count
displayedRows.removeRange(range)
let indexPaths = range.map{return NSIndexPath(forRow: $0, inSection: indexPath.section)}
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
}
}
}
else {
displayedRows.removeRange(range)
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
}
tableView.endUpdates()
}
viewModel.isCollapsed = !viewModel.isCollapsed
lastIndex = indexPath.row
}
I've tried variations of this code as well but for some reason keep getting the following error: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert row 6 into section 0, but there are only 6 rows in section 0 after the update'
I know that this is a datasource problem but I can't seem to find a solution. Any solution/or alternative way to accomplish this would be appreciated! Thanks
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()
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)
}