Inserting Rows crashes in Swift - swift

I am facing crash while inserting cells in TableView. I have tried below links but not sure what is the issue
Link1 Link2 Link3 and many
Below is my code
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = names[indexPath.row]
return cell
}
Data Source code is below
private var names: [String] = (50...99).map { String($0) }
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.appendCells()
}
}
private func appendCells() {
names = (0...49).map { String($0) } + names
customTableView.beginUpdates()
let indexPat = IndexPath(row: names.count - 1, section: 0)
customTableView.insertRows(at: [indexPat], with: .fade)
customTableView.endUpdates()
}
I cant understand what am I missing here. Sorry if I am doing foolish. Please let me know If I have to explain more.
I am getting error :
reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (100) must be equal to the number of rows contained in that section before the update (50),

The problem is the mismatch in the number of values you add to the data model and the number of rows you add to the table view.
The line:
names = (0...49).map { String($0) } + names
adds 50 new values to your data model.
But you then only tell the table view that there is one new row. This is what you are being told in the error message.
You need a loop to build up an array of 50 index paths.
private func appendCells() {
names = (0...49).map { String($0) } + names
var paths = [IndexPath]()
for row in 0...49 {
let indexPath = IndexPath(row: row, section: 0)
paths.append(indexPath)
}
customTableView.insertRows(at: paths, with: .fade)
}

The error clearly states that you are adding indexPath with row = 99, section = 0, while in reality, it's biggest indexpath holds values: row = 49, section = 0.
Before insertion, tableview calls datasource method numberOfRowsInSection - this method must return your newer, bigger array count (100), not the earlier one (50).

1) Your model before ‘appendCells’ has 50 names count.
2) After call your mode is equal 100 names count but you only instert one cell at index path = 99? You need to insert 50 more rows instead of one.

I was having this problem, and it worked with the following approach
func insertRow(entries: [String]) {
tableView.performBatchUpdates({
self.tableView.insertRows(at: [IndexPath(row: entries.count - 1, section: 0)], with: .bottom)
}, completion: nil)
}

Related

tableview swift uikit with firestore array

hello I have a problem creating a table view from firestore array of dictionary.
Note that the table view has the first cell that is a custom cell
for me the problem is because firestore array has only one dictionary as you could see here that is the the result of a print of the array plantDataArray
print("PLANT DATA ARRAY: \(plantDataArray)")
and I obtain this
PLANT DATA ARRAY: [Pietribiasi.PlantData(plantId: "C3884CIP01", plantType: "CIP", actualStatus: "WASHING", actualRecipe: "22")]
this is how I get the data from firestore and I put them on plantDataArray
func loadPlantData() {
db.collection("plantsData").whereField("plantId", isEqualTo: "C3884CIP01").getDocuments() { [self]
querySnapshot, error in
if let error = error {
print("Error: \(error.localizedDescription)")
}else{
self.plantDataArray = querySnapshot!.documents.compactMap({PlantData(dictionaryPlantData: $0.data()) })
DispatchQueue.main.async {
self.detailTableView.reloadData()
}
}
}
}
after I use this function
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return plantDataArray.count
}
and after this for generating the table view cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("INDEX ROW: \(indexPath.row)")
print("PLANT DATA ARRAY: \(plantDataArray)")
let customCell = detailTableView.dequeueReusableCell(withIdentifier: MyDetailTableViewCell.identifier, for: indexPath) as! MyDetailTableViewCell
customCell.selectionStyle = .none
let cell = detailTableView.dequeueReusableCell(withIdentifier: "DetailDefaultCell", for: indexPath)
switch indexPath.row {
case 0:
if plantDataArray[indexPath.row].plantType == "CIP"{
customCell.configure(imageName: "CIP")
} else if plantDataArray[indexPath.row].plantType == "PASTEURIZATION"{
customCell.configure(imageName: "PASTEURIZATION")
}
return customCell
case 1:
cell.textLabel?.text = "Actual Status:"
cell.detailTextLabel?.text = plantDataArray[indexPath.row].actualStatus
cell.detailTextLabel?.textColor = UIColor.gray
return cell
default:
return cell
}
}
but it generate only the first cell case 0 because plantDataArray.count is 1 so how I can solve this problem? It seams that I have to count the dictionary element and not the array of dictionary. Or what I'm doing wrong?
You want to have a row for each of the key value pairs in plantDataArray retrieved from Firestore. Currently, only "plantType" is shown instead of others. I think this is the intended behavior. Swift documentation for UITableView. Function tableView(UITableView, numberOfRowsInSection: Int) -> Int tells the data source to return the number of rows in a given section of a table view. In this case, you are returning "plantDataArray.count", which is 1 in this case. This means that the table view only contains 1 row, which makes sense why "switch indexPath.row" only returns case 0, as there is only 1 row in the view.
If you want to show all components in the same row, you will need to define the different properties in the tableViewCell. If you want the data to show in different rows, then you need to specify the exact number of rows, i.e. the number of keys in the plant data object.
For more information You can refer to the StackOverflow case1 and case2 where a similar issue related to population of tableview with data from firebase firestore has been discussed.

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.

Adding index list and section headers to translated tableview

How to add section headers and index list to UITableView in this use case?
#IBOutlet var tableView: UITableView!
var detail: Detail? = nil
var list = [tabledata]()
let search = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
list = [
tabledata(name:"something".localized, sort:"sort.something".localized, id:"something.html"),
tabledata(name:"somethingelse".localized, sort:"sort.somethingelse".localized, id:"somethingelse.html"),
...
]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "library", for: indexPath)
var data: tabledata
data = list[indexPath.row]
cell.textLabel!.text = data.name
return cell
}
Now the point is that table data are going to be translated.
Like if the user has set the language to English, sorting is unneeded;but if the language is set to German etc., section headers have to be applied to translated table.
Can someone figure out how to call a section letter from inside the dictionary, like
tabledata(section: "a", name:"anaconda".localized, sort:"sort.anaconda".localized, id:"anaconda.html")
Note that
name: is the actual cell name going to be .localized
sort: has to help characters like á é etc. in cell name to sort properly (avoiding them to show in the end of alphabet)
id: calls the html file location to display in detailViewController ('cause name has to be translated and we want a static text here)
A usual implementation of section headers and index list will result in something like
T // section header
translation // cell names
transmission
...
T // table in
Übersetzung // another language
Getriebe
...
Maybe there could be a function() which will get all the sections from tabledata and assign them to corresponding letters in var sectionletters = ["A", "B", ...] but that's above my beginner-limited knowledge.
What's the correct model for translated tableview?
Thanks for help!
There are a few ways to accomplish this. One of the simplest is to use the delegate functions provided with UITableView.
For organizational purposes I'd start by creating a class to represent sections. Something like this:
class GhostSection {
// a title for this section
var sectionTitle: String
// a list of items for this particular section
var items: [Ghost] = []
init(title: String, items: [Ghost]) {
sectionTitle = title
self.items = items
}
}
Then update the list variable to use the sections instead of a raw list of items:
var tabledata = [GhostSections]()
Ideally I'd create my GhostSection from a json object, but if it must be done manually then something like this would work:
// setup the table data to use GhostSection instances for each section
var tabledata = [
GhostSection(title: "a", items: [Ghost(name:"an".localized, sort:"sort.an".localized, id:"0101")]),
GhostSection(title: "b", items: [Ghost(name:"bc".localized, sort:"sort.bc".localized, id:"0102")]),
GhostSection(title: "c", items: [Ghost(name:"cd".localized, sort:"sort.cd".localized, id:"0103")])
]
I'd have numberOfSections(in tableView: UITableView) return the tabledata count, which would be the count of the total number of sections:
func numberOfSections(in tableView: UITableView) -> Int {
return tabledata.count
}
and the numberOfRowsInSection return the list count for each items array on each GhostSection in your tabledata array:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tabledata[section].items.count
}
Then I'd update cellForRow(at indexPath) to return the info for the right cell in that section:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "library", for: indexPath)
// this gets the section data, then gets the item from that section
var data = list[indexPath.section].items[indexPath.row]
cell.textLabel?.text = data.name
return cell
}
I also like to create my headers as reusable nib views, so the ListHeaderView just has a title label in it:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// this loads the view from the nib
guard let view = ListHeaderView.fromNib() as? ListHeaderView else {
return nil
}
// get the section object for this current section
let data = tabledata[section]
// and set the title label text here from our section object
view.titleLabel.text = data.sectionTitle
return view
}
// if you want to also have a custom footer you can do that here...
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return UIView()
}
// and let the header height be dynamic, or return a set height here...
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableViewAutomaticDimension
}
In case you want to use this approach here's how I implemented the fromNib() function so you can instantiate any view that has an associated .xib file:
public extension UIView {
public static func fromNib() -> UIView? {
let nibName = String(describing: self)
let bundle = Bundle(for: self)
let nib = UINib(nibName: nibName, bundle: bundle)
guard let view = nib.instantiate(withOwner: nil, options: nil).first as? UIView else {
print("Unable to instantiate nib named: \(nibName) in bundle: \(String(describing: bundle)) -- nib: \(String(describing: nib))")
return nil
}
return view
}
}
Also note it's a good idea to get in the habit of using camel case for class/object names with the first letter capitalized IF you want to keep with current conventions. So instead of naming your class ghost get in the habit of doing Ghost. For variables the convention is also camel case but start with a lowercase.
The easiest way is to use sections: one section for each first letter.
First, update your tableView(_:cellForRowAt:) to deal with sections: the first word in a section corresponds to the row value 0.
The section number corresponds to the letter: 0 for A, 1 for B, ...
In your code, you get an old cell correctly, but the way you set the text must be changed. You are doing something like that: cell.textLabel?.text = list[indexPath.row]. But this will not work because with sections, the indexPath.row parameter value is no longer the index of the ghost in your list: your list is a 1-dimension array, but with sections, UIKit uses a two dimensional way to give you the reference to the ghost: the arguments indexPath.row is now the index number of the ghost in the section indexPath.section. So if you have 4 ghost names, in your list, for instance "axx" (section 0, row 0), "ayy" (section 0, row 1), "bxx" (section 1, row 0) and "byy" (section 1, row 1), the values of the arguments to refer to "bxx", for instance, are 1 for indexPath.section and 0 for indexPath.row. With your current line of code, you will set the following text: list[0], so "axx". But it's "bxx" that corresponds to section 1 and row 0.
To get the correct behavior, you may process the whole list of ghosts, extract the ones that correspond to the letter for indexPath.section into another array, and finally get the element of this new array that has an index that equals to indexPath.row.
But as you see, this is a bit more tricky than we would like.
The best way to do all of that, easily, is to change your data source type: use a 2-dimensional array instead of your list that is of type [ghost].
You could for instance use something like:
var myNewDataSource : [String: [ghost]] = [
"A": [ghost1withNameStartingWithA, ghost2withNameStartingWithA, ...],
"B": [ghost1withNameStartingWithB, ghost2withNameStartingWithB, ...],
...
]
This way, everything is simpler.
Now, override this function, to have 26 letters or sections (you may customize this function if you want to display only letters containing at least one word):
override func numberOfSections(in tableView: UITableView) -> Int {
return 26
}
Override and implement correctly this function, to indicate how many words there are in a section, in your dictionary:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
[ COMPUTE THE NUMBER OF ROWS/WORDS FOR THIS LETTER AND RETURN THE VALUE ]
}
Then return a String with the letter for each section, in order to display this letter in the GUI:
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
[ RETURN THE LETTER CORRESPONDING TO THE SECTION: "A" for section 0, "B" for section 1, etc. ]
}
Finally, customize your sections, since you seems to want a specific background color and maybe a bold font or another font size, according to your screenshot:
override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let header = view as! UITableViewHeaderFooterView
header.backgroundView?.backgroundColor = UIColor(red: 253.0/255.0, green: 240.0/255.0, blue: 196.0/255.0, alpha: 1)
header.textLabel?.textColor = .black
header.textLabel?.font = UIFont(name: "Helvetica-Bold", size: 19)
}

When I scroll down in my TableView, the cell data changes - Swift 4

When I scroll down in my Table View, the cell data that has disappeared then changes. How can I solve this?
class TableViewController: UITableViewController {
var number = 1
let finishNumber = 10
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return finishNumber
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = "\(number)"
number = number + 1
return cell
}
}
You update number every time the table view asks for a cell. That has no direct relation to the row being displayed.
It's unclear why you even have the number property.
If you just want to show the corresponding row number in each cell, get rid of the number property and update:
cell.textLabel?.text = "\(number)"
with:
cell.textLabel?.text = "\(indexPath.row)"
Unclear question but I think you want to scroll to bottom when tableview reload right ?
extension UITableView {
func scrollToBottom(animated: Bool = false) {
let section = self.numberOfSections
if (section > 0) {
let row = self.numberOfRows(inSection: section - 1)
if (row > 0) {
self.scrollToRow(at: IndexPath(row: row - 1, section: section - 1), at: .bottom, animated: animated)
}
}
}
}
When calling, you need to use "async"
DispatchQueue.main.async(execute: {
yourTableView.reloadData()
yourTableView.scrollToBottom()
}
Good luck.

how can I trim my UITableViewController so that it shows always only 10 last rows?

Currently in my Swift app I have a UITableViewController. I already implemented paging when user scrolls to the top of the table - it loads more data and fills the table there.
But now I want to apply also the other feature - when user scroll to the very bottom of the table, it should truncate rows that added before thanks to paging and leave only last 10 cells.
So far my code looks like this:
override func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 { // first cell
print("this is first cell - it works")
loadMoreItems()
}
if indexPath.row == messages.count - 1 { // last cell
if(self.messages.count > 10){
leaveTenVisibleMessages()
}
}
let cell = tview.dequeueReusableCell(withIdentifier: "chat") as! SingleCommentCell
let msg = self.messages[indexPath.row]
.
.
.
and the method leaveTenVisibleMessages looks as follows (so far):
func leaveTenVisibleMessages(){
print("last cell before \(self.messages.count)")
if(self.messages.count > 10){
self.messages = Array(self.messages.suffix(10))
}
print("last cell after \(self.messages.count)")
//tview.reloadData()
}
however, even though I see that trimming the array worked:
last cell before 11
last cell after 10
I'm constantly getting error:
fatal error: Index out of range
I think the reason is that cellforrow at the moment of runtime expects more than 10 entries in array messages. How can I refresh only 10 rows then?
I just checked and the problem is this line:
let msg = self.messages[indexPath.row]
e.g. my code refers to the indexPath.row = 29 and messages array has only 10 records
Check your code that returns the numberOfRowsInSection.
e.g.
func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
let result = self.messages.count
NSLog("numberOfRowsInSection = %d", result)
return result
}