tableView.insertRows throws NSException, but not when `insertRows` at `[[0,0]]` - ios

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)

Related

UITableView reloadData() not working in swift?

I have a tableView which I am trying to populate from Firebase, but I have a problem with reloadData(),it does not refresh the table.
This is my code:
func showAnswers(pos: Int, name: String){
let n = name
answersReference.observe(DataEventType.value, with: {(snapshot) in
if(!snapshot.exists()){
}else{
let answers = snapshot.children
for answer in answers{
let answersFromDB = AnswerFromDBObject()
answersFromDB.setQuestion(question: (answer as! DataSnapshot).key)
let ansData = (answer as! DataSnapshot).children
for a in ansData{
answersFromDB.setAnswer(answer: (a as! DataSnapshot).key)
}
print("firebase answer: \(answersFromDB.getAnswer())")
self.answerFromDBObject += [answersFromDB]
}
print("array count: \(self.answerFromDBObject.count)")
self.userInfoAnwers.text = NSLocalizedString("users_questions", comment: "").replacingOccurrences(of: "(usernamehere)", with: n)
self.answersTable.reloadData()
}
})
}
And my tableView delegate methods:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return answerFromDBObject.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "answersCell") as! SeeAnswersCell
let answer = answerFromDBObject[indexPath.row]
cell.question.text = answer.getQuestion()
cell.answer.text = answer.getAnswer()
return cell
}
These are print methods on console:
firebase answer: Red
firebase answer: Fine
firebase answer: London
array count: 3
Try this after the final })
DispatchQueue.main.async {
self.answersTable.reloadData()
}
You are doing this to be able to reload it in the main thread
Check tableView delegate & data source connected
If you are using Xib cell, check if it's registered

UITableView reloadData() causing reload to UISearchBar inside every sections

I have a tableview which has 2 sections. Both of the sections have UISearchBar in the indexPath.row 0 and the rest of the rows in each section populate the list of array.
Whenever I type some text in search bar every time the searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) delegate method gets called and inside the delegate method I call tableView.reloadData() to reload the search results in tableview.
Now the problem is each time the tableView reloads the UISearchBar reloads too (as UISearchbar is in row number 1) and every time the SearchBar keypad Resigns.
Instead of doing tableView.reloadData() I even tried to reload every row except the first one using bellow code
let allButFirst = (self.tableView.indexPathsForVisibleRows ?? []).filter { $0.section != selectedSection || $0.row != 0 }
self.tableView.reloadRows(at: allButFirst, with: .automatic)
But no luck. App gets crashed saying
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert row 2 into section 0, but there are only 2 rows in section 0 after the update'
You are probably changing the data source and then you are reloading rows at index paths what doesn't exist yet.
It is not so easy, but let's have an example: Before you start typing, the search result will contain something like this:
["aa", "ab", "ba", "bb"]
Then you will type "a" to the search bar and data source changes into:
["aa", "ab"]
tableView.deleteRows(at: [IndexPath(row:3, section: 0), IndexPath(row:4, section: 0)], with: .automatic)
then you delete everything in this searchbar and your data source will change to the default: ["aa", "ab", "ba", "bb"]
so in this case you need to call:
tableView.insertRows(at: [IndexPath(row:3, section: 0), IndexPath(row:4, section: 0)], with: .automatic)
I created some working example - without storyboard source, I believe it is pretty simple to recreated it according this class.
class SearchCell: UITableViewCell {
#IBOutlet weak var textField:UITextField?
}
class TextCell: UITableViewCell {
#IBOutlet weak var label:UILabel?
}
class ViewController: UIViewController, UITableViewDataSource, UITextFieldDelegate {
#IBOutlet weak var tableView: UITableView?
weak var firstSectionTextField: UITextField?
var originalDataSource:[[String]] = [["aa","ab","ba","bb"], ["aa","ab","ba","bb"]]
var dataSource:[[String]] = []
let skipRowWithSearchInput = 1
override func viewDidLoad() {
super.viewDidLoad()
dataSource = originalDataSource
tableView?.tableFooterView = UIView()
tableView?.tableHeaderView = UIView()
}
func numberOfSections(in tableView: UITableView) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource[section].count + skipRowWithSearchInput
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0, let cell = tableView.dequeueReusableCell(withIdentifier: "search", for: indexPath) as? SearchCell {
cell.textField?.removeTarget(self, action: #selector(textFieldDidChangeText(sender:)), for: .editingChanged)
cell.textField?.addTarget(self, action: #selector(textFieldDidChangeText(sender:)), for: .editingChanged)
if indexPath.section == 0 {
firstSectionTextField = cell.textField
}
return cell
} else if let cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextCell {
cell.label?.text = dataSource[indexPath.section][indexPath.row - skipRowWithSearchInput]
return cell
} else {
return UITableViewCell()
}
}
#objc func textFieldDidChangeText(sender: UITextField) {
let section = sender == firstSectionTextField ? 0 : 1
let text = sender.text ?? ""
let oldDataSource:[String] = dataSource[section]
//if the search bar is empty then use the original data source to display all results, or initial one
let newDataSource:[String] = text.count == 0 ? originalDataSource[section] : originalDataSource[section].filter({$0.contains(text)})
var insertedRows:[IndexPath] = []
var deletedRows:[IndexPath] = []
var movedRows:[(from:IndexPath,to:IndexPath)] = []
//resolve inserted rows
newDataSource.enumerated().forEach { (tuple) in let (toIndex, element) = tuple
if oldDataSource.contains(element) == false {
insertedRows.append(IndexPath(row: toIndex + skipRowWithSearchInput, section: section))
}
}
//resolve deleted rows
oldDataSource.enumerated().forEach { (tuple) in let (fromIndex, element) = tuple
if newDataSource.contains(element) == false {
deletedRows.append(IndexPath(row: fromIndex + skipRowWithSearchInput, section: section))
}
}
//resolve moved rows
oldDataSource.enumerated().forEach { (tuple) in let (index, element) = tuple
if newDataSource.count > index, let offset = newDataSource.firstIndex(where: {element == $0}), index != offset {
movedRows.append((from: IndexPath(row: index + skipRowWithSearchInput, section: section), to: IndexPath(row: offset + skipRowWithSearchInput, section: section)))
}
}
//now set dataSource for uitableview, right before you are doing the changes
dataSource[section] = newDataSource
tableView?.beginUpdates()
if insertedRows.count > 0 {
tableView?.insertRows(at: insertedRows, with: .automatic)
}
if deletedRows.count > 0 {
tableView?.deleteRows(at: deletedRows, with: .automatic)
}
movedRows.forEach({
tableView?.moveRow(at: $0.from, to: $0.to)
})
tableView?.endUpdates()
}
}
the result:
If do you need to clarify something, feel free to ask in comment.
Try this-
tableView.beginUpdates()
//Do the update thing
tableView.endUpdates()
It worked.
I took two sections one for search field and another for reloading data (rows populating data).
I took separate custom cell for search and took outlet in that class itself.
and in viewForHeaderInSection I used tableView.dequeueReusableCell(withIdentifier:) and returned customCell.contentView
Then I called tableview.ReloadData() in searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
It worked without problem.

Custom UITableViewCell changes when scrolled

I have a bar button item which inserts new rows with an incrementing integer variable:
class TableViewController: UITableViewController {
var personNo = 0
var data = [String]()
#IBAction func addPerson(_ sender: UIBarButtonItem) {
personNo += 1
tableView.beginUpdates()
data.append("Person \(personNo)")
tableView.insertRows(at: [IndexPath(row: data.count - 1, section: 0)], with: .automatic)
tableView.endUpdates()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "newPerson") as! CustomCell
cell.lblPerson?.text = "Person \(personNo): "
// Configure the cell...
return cell
}
}
Adding the rows work, but the cell's value changes when the table view is scrolled:
Why is this happening and how can I save the state of each cell?
You only have a single personNo variable, so when cells are generated for scrolling, the current value of personNo is used.
You can use the indexPath.row value:
cell.lblPerson?.text = "Person \(indexPath.row+1): "
You need to get the data from the data source array (data)
Replace
cell.lblPerson?.text = "Person \(personNo): "
with
cell.lblPerson?.text = data[indexPath.row]
Side-note: for your purpose I recommend to use a custom model for example:
struct Person {
var name : String
var amount : Double
}

Inserting rows in UITableView upon click

I'm having trouble adding rows to the UITableView upon UIButton click.
I have two custom-cell xibs - one that contains an UILabel, another one that contains an UIButton.
Data for the table cell is loaded from two dictionaries (answersmain and linesmain).
Here is the code for the UITableView main functions:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.linesmain["Audi"]!.count + 1
}
// 3
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row < 3){
var cell:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
cell.lblCarName.text = linesmain["Audi"]![indexPath.row]
return cell
} else {
var celle:vwAnswers = self.tableView.dequeueReusableCellWithIdentifier("cell2") as! vwAnswers
celle.Answer.setTitle(answersmain["Good car"]![0], forState:UIControlState.Normal)
return celle
}}
What do I put here?
#IBAction func option1(sender: UIButton) {
// I need to add rows to the uitableview from two dictionaries into two different xibs
}
You can do the next:
var showingAll = false
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return showingAll ? self.linesmain["Audi"]!.count + 1 : 0
}
#IBAction func option1(sender: UIButton) {
showingAll = true
tableView.beginUpdates()
let insertedIndexPathRange = 0..<self.linesmain["Audi"]!.count + 1
var insertedIndexPaths = insertedIndexPathRange.map { NSIndexPath(forRow: $0, inSection: 0) }
tableView.insertRowsAtIndexPaths(insertedIndexPaths, withRowAnimation: .Fade)
tableView.endUpdates()
}
You should take a look over the documentation here
There is this UITableView method called insertRowsAtIndexPaths:withRowAnimation: that inserts row at a specified indexPath.
You need to modify linesmain and answersmain by adding data to these and then call [self.tableView reloadData].
It would be better if you extract linesmain["Audi"] and answersmain["Good car"] and save them into different mutable arrays and modify those.
You need to do this in the func option1.

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.

Resources