Applying NSDiffableDataSourceSnapshot to UITableViewDiffableDataSource cause 'NSInternalInconsistencyException' - uitableview

I am trying to implement a UITableViewViewDiffableDataSource for my tableview. My code compiles fine, however I keep running into this error the first time I apply a snapshot to it, with the following error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: section < [_state.dataSourceSnapshot numberOfSections]'
Here is my code:
BaseDisplayManager
class BaseDisplayManager: NSObject, UITableViewDelegate, BaseDiffableDataSourceDelegate {
var tableData = [[BaseViewModelProtocol]]()
var sections: [DiffableSection] = []
...
func setupDiffableDataSource(forTableView tableView: UITableView) {
dataSource = BaseDiffableDataSource(tableView: tableView, cellProvider: { (tableView, indexPath, model) -> UITableViewCell? in
guard let model = model as? BaseViewModelProtocol else { return nil }
let cell = tableView.dequeueReusableCell(withIdentifier: model.cellIdentifier, for: indexPath)
(cell as? TableViewCell)?.model = model
(cell as? NoSwipeTableViewCell)?.model = model
return cell
})
dataSource.delegate = self
}
func reloadSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<AnyHashable, AnyHashable>()
snapshot.appendSections(sections.map { $0.sectionIdentifier() })
sections.forEach { [weak self] section in
guard let items = self?.tableData[section.sectionIndex()] as? [AnyHashable] else { return }
snapshot.appendItems(items, toSection: section.sectionIdentifier())
}
dataSource.apply(snapshot, animatingDifferences: true)
}
DiffableSection
protocol DiffableSection {
func sectionIndex() -> Int
func sectionIdentifier() -> AnyHashable
}
BaseDiffableDataSource
protocol BaseDiffableDataSourceDelegate: class {
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
}
class BaseDiffableDataSource: UITableViewDiffableDataSource<AnyHashable, AnyHashable> {
weak var delegate: BaseDiffableDataSourceDelegate! = nil
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return delegate.tableView(tableView, canEditRowAt: indexPath)
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
return delegate.tableView(tableView, commit: editingStyle, forRowAt: indexPath)
}
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return delegate.tableView(tableView, canMoveRowAt: indexPath)
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
return delegate.tableView(tableView, moveRowAt: sourceIndexPath, to: destinationIndexPath)
}
}
}

I ran into the same issue in a ViewController which was a subclass of UITableViewController.
For me it was that I was configuring my datasource and applying a snapshot in viewWillAppear() AND calling super.viewWillAppear() before. Removing the super call did the trick.

Related

trailingSwipeActionsConfigurationForRowAt not work in stability

I am trying to make swipe-able table view cell for iOS 11 and above and trying to use the trailingSwipeActionsConfigurationForRowAt but it works on iPad and doesn't work on iPhone. It is not called in iPhone.
Surprisingly if I try to swipe the cell more then 10 or 20 times it sometimes works for once.
Here is my controller's table view extension
import UIKit
extension CustomViewController: UITableViewDelegate, UITableViewDataSource {
func initTableView() {
tableView.register(cell: SingleLineListItemViewCell.self)
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 64
tableView.rowHeight = UITableView.automaticDimension
tableView.backgroundColor = .green
tableView.isEditing = false
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = SingleLineListItemViewModel(title: "coko")
let cell = tableView.dequeueReusableCell(withIdentifier: SingleLineListItemViewCell.reuseIdentifier) as? SingleLineListItemViewCell
cell?.model = model
cell?.isFirst = indexPath.row == 0
cell?.isLast = indexPath.row == 2
return cell!
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 64.0
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
print("didSelectRowAt....")
router?.routeToTransfer()
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
#available(iOS 11.0, *)
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return nil
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
// Action here
// In case of delete, you can simply do:
if editingStyle == .delete {
//Remove item at relative position from datasource array
//Reload tableview at the respective indexpath
}
}
#available(iOS 11.0, *)
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { _, _, _ in
//YOUR_CODE_HERE
}
deleteAction.backgroundColor = .red
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
}
Any help would be much appreciated.
The only obvious thing wrong with this is that you're not calling the completion handler. According to the API documentation it is necessary to call the completion handler to indicate whether the operation was successful. What it actually does I have no idea...
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { _, _, completionHandler in
//YOUR_CODE_HERE
completionHandler(true)
}
deleteAction.backgroundColor = .red
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
If this doesn't make any difference, do you have any other gesture recognisers that could be conflicting with the swipe action?
you can try this one
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? {
return "Delete"
}
// this method handles row deletion
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// remove the item from the data model
// delete the table view row
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Not used in our example, but if you were adding a new row, this is where you would do it.
}
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let action = UIContextualAction(
style: .normal,
title: "Delete",
handler: { (action, view, completion) in
//do what you want here
completion(true)
})
action.backgroundColor = .red
let configuration = UISwipeActionsConfiguration(actions: [action])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
Here is are actions defined as well (point 1 and 2);
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "") { _, _, completionHandler in
// 1. remove object from your array
scannedItems.remove(at: indexPath.row)
// 2. reload the table, otherwise you get an index out of bounds crash
self.tableView.reloadData()
completionHandler(true)
}
deleteAction.backgroundColor = .systemOrange
deleteAction.image = UIImage(named: "trash")
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
configuration.performsFirstActionWithFullSwipe = true
return configuration
}

UITableView not showing Delete button on clicking minus button(Only on iOS 13)

I have a table view to shows a list of items. I have implemented the default editing style delete. It's been working till iOS 12. But not on iOS 13. It does not show the Delete button when clicking the minus button.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return machineOrders.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MachineOrderCell") as! MachineOrderCell
let machineOrder = machineOrders[indexPath.row]
cell.update(with: machineOrder)
cell.decreaseQuantityButton.isEnabled = canDecreaseQuantity(for: machineOrder)
cell.onDecreaseQuantity = {
self.delegate.view(self, didSelectDecreaseQuantity: machineOrder)
}
cell.onIncreaseQuantity = {
self.delegate.view(self, didSelectIncreaseQuantity: machineOrder)
}
return cell
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
navigationItem.rightBarButtonItem = doneBarButton
}
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
navigationItem.rightBarButtonItem = !isCartEmpty ? (tableView.isEditing ? doneBarButton : editBarButton) : nil
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
let machineOrder = machineOrders[indexPath.row]
if editingStyle == .delete {
self.delegate.view(self, didSelectDelete: machineOrder)
}
}
Press minus button for some seconds, I mean long press, then it will work.

Reset the reordered TableView rows on button click

I am implementing a tableview which it shows a lists of documents:
MyTableviewController
import UIKit
class MyTableViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var editButton: UIBarButtonItem!
#IBOutlet weak var tableview: UITableView!
var animalNameArray = ["cat","dog","lion"]
override func viewDidLoad() {
super.viewDidLoad()
tableview.delegate = self
tableview.dataSource = self
}
#IBAction func editButtonAtNavigationBar(_ sender: UIBarButtonItem) {
self.tableview.isEditing = !self.tableview.isEditing
sender.title = (self.tableview.isEditing) ? "Done" : "Edit"
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return animalNameArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let valueAtCell = tableView.dequeueReusableCell(withIdentifier: "myCustomCell", for: indexPath) as! CustomTableViewCell
valueAtCell.cellLabel?.text = animalNameArray[indexPath.row]
return valueAtCell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
animalNameArray.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
//Rearranging the table view cells
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let itemMove = animalNameArray[sourceIndexPath.row]
animalNameArray.remove(at: sourceIndexPath.row)
animalNameArray.insert(itemMove, at: destinationIndexPath.row)
}
}
I want to reset the reordered lists clicking on cancel button. How?
When I close the application and start it again (any time refresh data function runs), rows comes back to their default places while I need to have saved the changes in reordering.
And how can i use image (trash) at the position of delete??
import UIKit
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var editButton: UIBarButtonItem!
#IBOutlet weak var tableview: UITableView!
var animalNameArray = [String]() {
didSet {
UserDefaults.standard.set(animalNameArray, forKey: "savedNameArray")
}
}
var originalArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
tableview.delegate = self
tableview.dataSource = self
animalNameArray = UserDefaults.standard.array(forKey: "savedNameArray") as? [String] ?? ["cat","dog","lion"]
originalArray = animalNameArray
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
#IBAction func editButtonAtNavigationBar(_ sender: UIBarButtonItem) {
self.tableview.isEditing = !self.tableview.isEditing
sender.title = (self.tableview.isEditing) ? "Done" : "Edit"
}
#IBAction func cancelButtonAtNavigationBar(_ sender: UIBarButtonItem) {
self.tableview.isEditing = false
navigationItem.rightBarButtonItem?.title = "Edit"
animalNameArray = originalArray
tableview.reloadData()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return animalNameArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let valueAtCell = tableView.dequeueReusableCell(withIdentifier: "myCustomCell", for: indexPath) as! CustomTableViewCell
valueAtCell.cellLabel?.text = animalNameArray[indexPath.row]
return valueAtCell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
animalNameArray.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
//Rearranging the table view cells
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let itemMove = animalNameArray[sourceIndexPath.row]
animalNameArray.remove(at: sourceIndexPath.row)
animalNameArray.insert(itemMove, at: destinationIndexPath.row)
}
}
Try this code, and connect your cancel button from storyboard to #IBAction func cancelButtonAtNavigationBar(_ sender: UIBarButtonItem)
Here is the solution
import UIKit
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var editButton: UIBarButtonItem!
#IBOutlet weak var tableview: UITableView!
var animalNameArray = ["cat","dog","lion"]
override func viewDidLoad() {
super.viewDidLoad()
tableview.delegate = self
tableview.dataSource = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let defaults = UserDefaults.standard
let operationArray = defaults.stringArray(forKey: "SavedStringArray") ?? [String]()
if operationArray.count == 0 {
}else{
animalNameArray = operationArray
}
}
#IBAction func editButtonAtNavigationBar(_ sender: UIBarButtonItem) {
self.tableview.isEditing = !self.tableview.isEditing
sender.title = (self.tableview.isEditing) ? "Done" : "Edit"
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return animalNameArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let valueAtCell = tableView.dequeueReusableCell(withIdentifier: "myCustomCell", for: indexPath) as! CustomTableViewCell
valueAtCell.cellLabel?.text = animalNameArray[indexPath.row]
return valueAtCell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
animalNameArray.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
print(animalNameArray)
let defaults = UserDefaults.standard
defaults.set(animalNameArray, forKey: "SavedStringArray")
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
//Rearranging the table view cells
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let itemMove = animalNameArray[sourceIndexPath.row]
animalNameArray.remove(at: sourceIndexPath.row)
animalNameArray.insert(itemMove, at: destinationIndexPath.row)
print(animalNameArray)
}
}
Download sample https://github.com/testingraahul/TableViewEditing/tree/master

UICollectionView inside UITableviewCell after moving the indexpath

After following a guide on how to add collectionViews inside a tableViewCell (here) and then I tried to see if I can switch these two cells.
However, when I try to scroll down the collectionview(horizontally) I get an error that the collectionview index goes out of bounds. I tried to see if adding a reload data for each collection view would help with that issue, but it doesn't... Code below:
class BuildTravelPackage: UIViewController, UITableViewDataSource,UITableViewDelegate, UICollectionViewDelegate {
#IBOutlet var optionsTableView: UITableView!
var items = [[PackageOption]]()
override func viewDidLoad() {
super.viewDidLoad()
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPress(longPressGestureRecognizer:)))
self.optionsTableView.addGestureRecognizer(longPressRecognizer)
//load data into cells
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.alpha = 0
UIView.animate(withDuration: 1.0) {
cell.alpha = 1.0
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 175
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
return .none
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = items[sourceIndexPath.row]
items.remove(at: sourceIndexPath.row)
items.insert(item, at: destinationIndexPath.row)
optionsTableView.isEditing = !optionsTableView.isEditing
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = optionsTableView.dequeueReusableCell(withIdentifier: "optionCell", for: indexPath) as! PackageOptionTableViewCell
cell.collectionView.delegate = self
cell.collectionView.dataSource = self
cell.collectionView.tag = indexPath.row
cell.collectionView.reloadData()
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Column:",indexPath.row)
print("Row:",collectionView.tag)
//highlght selected cell and load a new cell with infomation? Or load a uiview with date selection
}
func longPress(longPressGestureRecognizer: UILongPressGestureRecognizer) {
if longPressGestureRecognizer.state == UIGestureRecognizerState.began {
let touchPoint = longPressGestureRecognizer.location(in: self.view)
if let indexPath = optionsTableView.indexPathForRow(at: touchPoint) {
optionsTableView.isEditing = !optionsTableView.isEditing
}
}
}
}
extension BuildTravelPackage:UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items[collectionView.tag].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "singleOptionCell", for: indexPath) as! OptionCollectionViewCell
cell.image.image = items[collectionView.tag][indexPath.row].imageFile
cell.label.text = items[collectionView.tag][indexPath.row].name
return cell
}
}
I think problem is here when you are replacing objects
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = items[sourceIndexPath.row]
items.remove(at: sourceIndexPath.row)
items.insert(item, at: destinationIndexPath.row)
optionsTableView.isEditing = !optionsTableView.isEditing
}
try replacing code by
swap(& items[sourceIndexPath.row], & items[destinationIndexPath.row])

UITableView Force Right-to-Left swipe to delete not work

I have uiviewcontroller with uitableview
and swipe to delete work fine but
when I set uitableview semantic to force right-to-left the swipe to delete not even shown in the tableview
classViewController:UIViewController,UITableViewDelegate,UITableViewDataSource {
var yourArray : [Type] = []
override func viewDidLoad() {
super.viewDidLoad()
tablView.delegate = self
tablView.dataSource = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return yourArray.count
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete{
tableView.beginUpdates()
yourArray.remove(at: indexPath.row)
tableView.endUpdates()
tableView.reloadData()
}
}
}

Resources