UITableView reloadRows crashes sometime - swift

I have 4 rows which shows and hide some data below the selected row. Its works fine Most of the time. But when I click second/third/fourth rows multiple times(like 13-14) times and then click on first row then sometime it crashes not always but it crashes most of the time after multiple clicks. I am changing row height as well. Crash only happen when I select first row(case a), after selecting other rows multiple times. Below is my code -
I tried multiple things like calling it between beginUpdate and endUpdate, check is indexPath exist before reloading
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ”cell”, for: indexPath) as! CustomTableViewCell
let model = dataSource[indexPath.row]
cell.model = model
switch model.type {
case .a:
cell.aList = savedCardList
cell.addACallback = {[weak self] (sender) in
self?.moveToAVC()
}
cell.addACallback= {[weak self] (a) in
self?.selectedA = a
self?.updateButton()
}
case .b:
cell.bList = bList
cell.bSelectedCallback = {[weak self] (bInfo) in
self?.selectedB= bInfo
self?.updateButton()
}
case .c:
cell.cList = cList
if let cInfo = selectedC {
cell.selectOtherCButton.setTitle(cInfo.name, for: .normal)
}
cell.selectOtherCCallback = {[weak self] (sender) in
self?.showSelectCVC()
}
cell.CSelectedCallback = {[weak self] (cInfo) in
self?.selectedC = cInfo
self?.updateButton()
}
case .d:
cell.dTextField.text = dString
cell.dChangedCallBack = {[weak self] (textField) in
if let dStr = textField.text {
self?.dString = dStr
}
self?.updateButton()
}
default:
break
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let model = dataSource[indexPath.row]
guard model.isSelected == true else {
return 90
}
switch model.type {
case .a:
if aList.count > 0 {
return 350
}
return 140
case .b:
return 200
case .c:
return 270
case .d:
return 130
default:
return 90
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let model = dataSource[indexPath.row]
selectedType = model.type
updateButtonState(false)
updateButton()
if let selectedIndexPath = selectedIndexPath, selectedIndexPath != indexPath {
self.updatemodelSelectedState(false, at: selectedIndexPath)
self.updatemodelSelectedState(true, at: indexPath)
if let _ = tableView.cellForRow(at: indexPath), let _ = tableView.cellForRow(at: selectedIndexPath) {
self.reloadTableViewRows([selectedIndexPath, indexPath])
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
else if !model.isSelected {
self.updatemodelSelectedState(true, at: indexPath)
if let _ = tableView.cellForRow(at: indexPath) {
self.reloadTableViewRows([indexPath])
}
}
selectedIndexPath = indexPath
}
private func reloadTableViewRows(_ indexPaths: [IndexPath]) {
DispatchQueue.main.async {
self.myTableView.beginUpdates()
self.myTableView.reloadRows(at: indexPaths, with: .automatic)
self.myTableView.endUpdates()
}
}
Xcode console log(I am only getting this one liner) -
libc++abi.dylib: terminating with uncaught exception of type
NSException
It crashes at self.myTableView.endUpdates() this line. If I remove begin and end updates then it crashes at self.myTableView.reloadRows(at: indexPaths, with: .automatic). Also for one of the row I show a popup on a button click and after then if I select my first row, it crashes!
Also I have collection view inside table view rows which I am hiding and showing. Even though its flow is set to horizontal, after multiple clicks once in a while I notice that collection view rows comes in vertical flow and then in next click it gets back to horizontal flow again. Not sure why this is happening but could this be issue?
Please help. I am trying to resolve this issue from last 2-3 hours :(

Try This one i hope it will be work for you:
UIView.setAnimationsEnabled(false)
tableView.beginUpdates()
tableView.reloadRows(at: [indexPath!], with: .none)
tableView.endUpdates()
UIView.setAnimationsEnabled(true)

Related

Issue in DiffableDataSource tableview header

I'm using the tableView using DiffableDataSource and only issue which I've faced in header.I'm using viewForheaderSections method for calling header header view appear but on bottom position not top of the list please see the code thanks.
extension SinlgeTableViewcontroller:UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: Header.self)) as? Header else {return UIView()}
cell.lblHeader.text = "Top Mens"
return cell
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 40
}
func createDataSource(){
dataSource = UITableViewDiffableDataSource<ProductsSections,AnyHashable>(tableView: tableView, cellProvider: { tableView, indexPath, itemIdentifier in
switch self.sectionsData[indexPath.section]{
case .first:
guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ProductCell.self)) as? ProductCell else {return UITableViewCell()}
cell.products = self.products[indexPath.row]
return cell
case .second:
guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MensCell.self)) as? MensCell else {return UITableViewCell()}
cell.mens = self.mens[indexPath.row]
return cell
}
})
}
func createSnapshot(){
var snapshot = NSDiffableDataSourceSnapshot<ProductsSections,AnyHashable>()
sectionsData.forEach{ $0
snapshot.appendSections([$0])
}
// snapshot.appendSections([.first,.second])
snapshot.appendItems(products, toSection: .first)
snapshot.appendItems(mens, toSection: .second)
dataSource?.apply(snapshot, animatingDifferences: true, completion: nil)
}
According to your code you are not using viewForHeaderInSection. Instead of that you are using viewForFooterInSection. So instead of a header, a footer will appear in the bottom.
If you only need a header change all the methods related to the footer to the header.

NSFetchedResultsController error: 'no object at index in section at index 0'

I have a UITableView which populate it cells with a NSFetchedResultsController based on CoreData attribute isForConverter which is Bool and should be true to be displayed. isForConverter state sets in another ViewController.
When I want to delete some cells from the UITableView and after access cells which wasn't deleted I receive the error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'no object at index 5 in section at index 0'
There is a GIF with the problem: https://cln.sh/M1aI9Z
My code for deleting cells. I don't need to delete it from database, just change it isForConverter from true to false:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let currency = fetchedResultsController.object(at: indexPath)
currency.isForConverter = false
coreDataManager.save()
}
}
NSFetchedResultsController Setup and delegates:
func setupFetchedResultsController() {
let predicate = NSPredicate(format: "isForConverter == YES")
fetchedResultsController = coreDataManager.createCurrencyFetchedResultsController(with: predicate)
fetchedResultsController.delegate = self
try? fetchedResultsController.performFetch()
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .none)
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .none)
}
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .none)
}
default:
tableView.reloadData()
}
}
}
I noticed that if I just add tableView.reloadData() to:
tableView(_ tableView: UITableView, commit editingStyle:
UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
Then everything works good. But deletion animation is really fast and antsy. Also according to docs I should not use tableView.reloadData() with NSFetchedResultsController...
How to fix that behaviour?
UPDATE:
It seems I found out what the reason of that crash was. This is what my print() tryings gave: SCREENSHOT.
What is a pickedCurrency: this is a global variable of custom type Currency which I created to receive its attribute currentValue (Double, 87.88). I need that value only from the picked to edit cell. After I use that value for calculation at cellForRowAt() and result of the calculation fills all other cells which is not in the edit mode now.
I define pickedCurrency in textFieldDidBeginEditing() because there I receive the exact row of Currency I picked to edit:
func textFieldDidBeginEditing(_ textField: UITextField) {
pickedCurrency = fetchedResultsController.object(at: IndexPath(row: textField.tag, section: 0))
numberFromTextField = 0
textField.textColor = UIColor(named: "BlueColor")
textField.placeholder = "0"
textField.text = ""
}
And then use it's value in cellForRowAt to calculate all other cells values based on pickedCell value and number I put in a textField of activeCell:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "converterCell", for: indexPath) as! ConverterTableViewCell
let currency = fetchedResultsController.object(at: indexPath)
cell.flag.image = currencyManager.showCurrencyFlag(currency.shortName ?? "notFound")
cell.shortName.text = currency.shortName
cell.fullName.text = currency.fullName
cell.numberTextField.tag = indexPath.row
cell.numberTextField.delegate = self
if let number = numberFromTextField, let pickedCurrency = pickedCurrency {
cell.numberTextField.text = currencyManager.performCalculation(with: number, pickedCurrency, currency)
}
return cell
}
It seems when I delete a lot of cells and then click on random cell to edit it's not updates its IndexPath(row: textField.tag, section: 0)...
Maybe there is a way to receive Currency object I picked for editing in cellForRowAt()?
I am not certain if this will work but it seems too long for it to be a comment so give the below a try.
Initially I told you could use tag to identify a specific view which is good for quick and simple implementations but when rows get moved / deleted as we have it now, it will be very difficult to manage using tags as you have to constantly update them.
For static table views, they are fine but if your rows will change, then more sophisticated patterns like delegate / observer might be better.
Anyways, what I think can help your situation for now in textFieldDidBeginEditing is stop using the tag to get the index path and get the indexpath from what is tapped.
I still think maybe delegate pattern is better but this might work for you:
func textFieldDidBeginEditing(_ textField: UITextField) {
// Get the coordinates of where we tapped in the table
let tapLocation = textField.convert(textField.bounds.origin,
to: tableView)
if let indexPath = self.tableView.indexPathForRow(at: tapLocation)
{
// don't use tag of textfield anymore
pickedCurrency
= fetchedResultsController.object(at: IndexPath(row: indexPath,
section: 0))
numberFromTextField = 0
textField.textColor = UIColor(named: "BlueColor")
textField.placeholder = "0"
textField.text = ""
}
}
Does this help ?

Inserting cell in specific section

I am trying to find a way that allows me to insert a new cell under a specific section on my tableview. There is a button that is pressed called "add song" and once a user presses on that it should insert a new cell that is built by with a prototype cell. That prototype cell will allow a user to click on it and edit certain information on that cell. I have been trying to code a way to insert the cell below the cell that is currently in that section which is section "3". I'm sure it is something simple that I am messing up since I'm not very use to doing tableviews. Here is my code:
import UIKit
class MultipleSongsTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addSong(_ sender: Any) {
insertNewSongCell()
}
func insertNewSongCell() {
let indexPath = IndexPath(row: -1, section: 3)
tableView.beginUpdates()
tableView.insertRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
}
extension MultipleSongsTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 1
} else if section == 1 {
return 1
} else if section == 2 {
return 1
} else if section == 3 {
return 1
} else {
return 1
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "trackTitleCell", for: indexPath) as! ReleaseTitleTableViewCell
return cell
} else if indexPath.section == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "genreCell", for: indexPath) as! Genre2TableViewCell
return cell
} else if indexPath.section == 2 {
let cell = tableView.dequeueReusableCell(withIdentifier: "TrackListCell", for: indexPath) as! TrackListTableViewCell
return cell
} else if indexPath.section == 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "TrackListSongCells", for: indexPath) as! TrackListSongsTableViewCell
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "AddSongButtonCell", for: indexPath) as! AddSongTableViewCell
return cell
}
}
}
Also here is a screenshot of how my viewcontroller looks and where I expect the new cell to populate.
I would like the new cell to be inserted after the cell that says "Song Name", I would like the inserted cell to be the same prototype cell that is currently there because the user can click on that cell and fill out information and change the current "Song Name" label to what ever they want.
First of all you need a data source array for the section for example
var songs = [String]()
Then you have to modify numberOfRowsInSection to return the number of songs for section 3. This method can be simplified anyway
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 3: return songs.count
default: return 1
}
}
Now you can add a new song to the array and insert the row
func insertNewSongCell() {
let insertionIndex = songs.count
songs.append("New Song")
let indexPath = IndexPath(row: insertionIndex, section: 3)
tableView.insertRows(at: [indexPath], with: .automatic)
}
beginUpdates and endUpdates have no effect in this case, you can omit the lines.
Calling UITableView.insertRows(at:with:) will insert cells into your UITableView. You can insert exactly 1 cell by passing an indexPaths argument containing 1 IndexPath:
self.tableView.insertRows(at: [IndexPath(row: 1, section: 3)], with: .automatic)
You may need to also update your UITableViewDataSource to return expected values, e.g. from its tableView(_:numberOfRowsInSection:), to account for the additional row(s). Otherwise, your app will throw an unhandled exception and crash.

selecting cell in section one authomatically selecting cell in section two swift

I have an application where I have two sections the issue I have now is if I select an item in section 1, it automatically selects a cell in section 2 which is not suppose to be. I want Items to be selectable in section 1 without affecting section two.
below is my selection
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.section {
case 0:
showCustomDialog(subD: sub[indexPath.row])
case 1:
let cell = tableView.cellForRow(at: indexPath) as! VasListCell
cell.checkBox.setOn(true, animated: true)
default: break
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
switch indexPath.section {
case 1:
let cell = tableView.cellForRow(at: indexPath) as! VasListCell
cell.checkBox.setOn(false, animated: true)
default: break
}
}
where I am using the selected index
func selectedIndex(viewcontroller: UIViewController) {
let selectedRows = tableView.indexPathsForSelectedRows
guard let vasRow = selectedRows?.map ({ vas[$0.row] }) else { return }
selectedVasData = vasRow
let vasData = selectedVasData
let subData = selectedSubData
let vcr = viewcontroller as! CheckoutVC
vcr.vas = vasData
vcr.sub = subData
let tot1 = subData.compactMap {$0.price}
let tot2 = vasData.compactMap {$0.amount}
let tot = tot1 + tot2
let reduced = tot.compactMap(Double.init).reduce(0, +)
vcr.tableView.reloadData()
self.present(viewcontroller, animated: true, completion: nil)
print("CELL INDEX vas \(StaticFunc.convertDoubleToCurrency(amount: reduced))")
}

Error when deleting a UITableViewRow, index of row clicked not in range of possible indexes

I have a problem when deleting an item manually using a custom button, when the amount of items (variable of the cell), happening when the user clicks the minus sign at a value of that variable == 1. The cells correctly slide up as the previous one gets deleted, but then when all the cells below go up, the indexes do not change, even though I correctly remove both the cell and the value from my dictionary.
I tried to unable user activity on the screen, but still, that was not the issue as I understand it is a type index out of range error.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
ordersTableView.register(UINib(nibName: "OrdersTableViewCell", bundle: .main), forCellReuseIdentifier: "ordersTableViewCell")
let cell = ordersTableView.dequeueReusableCell(withIdentifier: "ordersTableViewCell", for: indexPath) as! OrdersTableViewCell
let amount = dishesDictionary[indexPath.item]![4] as! Int
cell.dishAmountLabel.text = ("\(String(amount))x")
#objc func decreaseAmount(sender: UIButton){
let index = sender.tag
let indexPath = IndexPath(item: index, section: 0)
if (dishesDictionary[index]![4] as! Int) > 1 {
let numberOfItems = dishesDictionary[index]![4] as! Int
dishesDictionary[index]![4] = numberOfItems - 1
ordersTableView.reloadRows(at: [indexPath], with: .fade)
print(dishesDictionary[index])
print("COUNT: \(dishesDictionary.count)")
}
else if (dishesDictionary[index]![4] as! Int) == 1 {
ordersTableView.beginUpdates()
self.dishesDictionary.removeValue(forKey: index)
self.ordersTableView.deleteRows(at: [indexPath], with: .fade)
ordersTableView.endUpdates()
UIApplication.shared.beginIgnoringInteractionEvents()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7, execute: {
self.ordersTableView.reloadData()
UIApplication.shared.endIgnoringInteractionEvents()
})
}
else {
return
}
calculateTotalAmount()
}