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.
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)
}
I thought this would be a straight forward thing to do but for some unknown reason, I can't access the properties of the object at a given indexPath.
I'm trying to create my app using MVVM as much as possible (still using Swift 2.3) so my UITableViewController is backed by a view model called FormulasViewModelFromLiftEvent. The view model is initialized with a fetchedResultsController like so:
struct FormulasViewModelFromLiftEvent {
var modelLiftEvent: LiftEventProtocol
var fetchedResultsController: NSFetchedResultsController
let dataManager = CoreDataHelper()
init(withLiftEvent liftEventModel: LiftEventProtocol) {
self.modelLiftEvent = liftEventModel
let moc = modelLiftEvent.context
fetchedResultsController = dataManager.fetchSelectableFormulas(moc)
}
}
The results are a collection of Formula objects.
Back in my UITableViewController, I have this extension in which I'm putting the required table view methods. I can access the fetchResultsController of the view model within the numberOfSectionsInTableView and numberOfRowsInSection methods with no problems. In cellForRowAtIndexPath, I want to populate the cell with the formula name property but I can't access any of the properties of the objectAtIndexPath:
extension FormulasViewController: UITableViewDataSource {
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
guard let sections = viewModel.fetchedResultsController.sections else { return 0 }
return sections.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = viewModel.fetchedResultsController.sections?[section] else { return 0 }
return section.numberOfObjects
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellViewModel: SelectionsRepresentable
let cell = tableView.dequeueReusableCellWithIdentifier("formulasCell", forIndexPath: indexPath)
let formula = viewModel.fetchedResultsController.objectAtIndexPath(indexPath)
let formulaName = formula. <-- None of the Formula properties are available
cellViewModel = FormulaSelectionViewModel(defaultFormulaName: formulaName)
cell.textLabel?.text = cellViewModel.text
cell.accessoryType = cellViewModel.accessoryType
return cell
}
}
According to the documentation, let formula = viewModel.fetchedResultsController.objectAtIndexPath(indexPath) should return the Formula object at the given index path in the fetch results. But formula thinks it's an NSFetchRequestResult and so of course I can't access any Formula properties.
NSFetchRequestResult is the protocol NSManagedObject, NSDictionary, NSManagedObjectID and NSNumber conform to. You need to type cast the object:
let formula = viewModel.fetchedResultsController.objectAtIndexPath(indexPath) as! Formula
So i have an array of struct where i add data from my json. There are different types of data in the same array.
struct PersonData {
let name: String
let age: String
let sex : String
}
What i want to do is to implement a pickerView that will reload the table view depending on what the user choose. Lets say i have 2 picker views where the user can choose sex and age.
So if he chose all males with 17 years old i want to show only that on the table view.
I can already get the count on the array but i can't return nil on the UITableViewCell method
I wanted to do something like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseCell", for: indexPath) as! CellTeatroTableViewCell
if(option == 0) //All persons. Default option
{
cell.name.text = dataArray[indexPath.row].name
return cell
}
else
{
if(dataArray[indexPath.row].age == agePickerOption || dataArray[indexPath.row].sex == sexPickerOption )
{
cell.name.text = dataArray[indexPath.row].name
return cell
}
}
return nil
}
I know i cant return nil since he is expecting a UITableViewCell but its possible to only increment the indexPath if certain conditions are met?
Or i need to add the cell and delete it right after? This doesn't sounds right.
I would have two data arrays:
allDataArray - all elements
filteredDataArray - elements that comply to your filter
If you use the filteredArray as a DataSource you dont have to put that logic in the cellForRow Method.
Instead, use the picker delegate methods to filter your allDataArray and put it on the filteredDataArray.
I have a UITableView created with 2 prototype cells, each of which have separate identifiers and subclasses.
My problem is when I display the cells the second prototype's first row gets absorbed under the first prototype cell.
For example, I have the first prototype cell displaying only 1 item. The second prototype cell should display 4 items. But, the first item from the second prototype cell is not displaying and, instead, there are only 3 of the four items visible.
Here is the code I have implemented:
override func viewDidLoad() {
super.viewDidLoad()
self.staticObjects.addObject("Please...")
self.objects.addObject("Help")
self.objects.addObject("Me")
self.objects.addObject("Thank")
self.objects.addObject("You")
self.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.objects.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row==0){
let staticCell = self.tableView.dequeueReusableCellWithIdentifier("staticCell", forIndexPath: indexPath) as! StaticTableViewCell
staticCell.staticTitleLabel.text = self.staticObjects.objectAtIndex(indexPath.row) as? String
return staticCell
}
else{
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! PillarTableViewCell
cell.titleLabel.text = self.objects.objectAtIndex(indexPath.row) as? String
return cell
}
Thanks for all the help.
You have logic issues with how you are counting the number of rows in your table for both tableView:numberOfRowsInSection and tableView:cellForRowAtIndexPath.
Your code is producing a display output as shown below where:
The blue cells represent your staticCell prototype table cell view; these are the values from the staticsObjects array.
The yellow cells represent your cell prototype table cell view; these are the values from the objects array.
1. Look at tableView:cellForRowAtIndexPath:
In the cellForRowAtIndexPath method, you are only returning the count of the objects array.
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.objects.count
}
That means that the number of rows you will have in your table will be 4 instead of 5. Instead, you want to return the sum of the two arrays you are using in your table: objects.count + staticObjects.count. For example:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count + staticObjects.count
}
2. Look at tableView:cellForRowAtIndexPath:
Here's your original code with my comments..
// You are assuming that `staticObjects` will always have
// exactly one row. It's better practice to make this
// calculation more dynamic in case the array size changes.
if (indexPath.row==0){
let staticCell = self.tableView.dequeueReusableCellWithIdentifier("staticCell", forIndexPath: indexPath) as! StaticTableViewCell
staticCell.staticTitleLabel.text = self.staticObjects.objectAtIndex(indexPath.row) as? String
return staticCell
} else {
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! PillarTableViewCell
// Here's your problem! You need to calculate the row
// because you want to put the objects from your other
// array first. As a result, you need to account for them.
cell.titleLabel.text = self.objects.objectAtIndex(indexPath.row) as? String
return cell
}
Now, here's one way to fix your errors stated in the above discussion:
// If the indexPath.row is within the size range of staticObjects
// then display the cell as a "staticCell".
// Notice the use of "staticObjects.count" in the calculation.
if indexPath.row < staticObjects.count {
let staticCell = self.tableView.dequeueReusableCellWithIdentifier("staticCell", forIndexPath: indexPath) as! StaticTableViewCell
staticCell.staticTitleLabel.text = self.staticObjects.objectAtIndex(indexPath.row) as? String
return staticCell
} else {
// If you get here then you know that your indexPath.row is
// greater than the size of staticObjects. Now you want to
// display your objects values.
// You must calculate your row value. You CANNOT use the
// indexPath.row value because it does not directly translate
// to the objects array since you put the staticObjects ahead
// of them. As a result, subtract the size of the staticObjects
// from the indexPath.row.
let row = indexPath.row - staticObjects.count
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! PillarTableViewCell
cell.titleLabel.text = self.objects.objectAtIndex(row) as? String
return cell
}
Now you should see this: