Add a cell only if certain conditions are met - swift

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.

Related

Reusable UITableView for Varying Data Input - Swift/Xcode

I have a TableView that I want to reuse for different categories of data (essentially as plugins.. the tableView being a skeleton and being filled with whatever I want it to be). The TableView is filled with different categories of data (and related actions) depending on essentially what ViewController the user came from. I understand how to make it display the various data (just send it whatever array I want it to display), but I can't figure out how I could control the actions for the data at the specific index selected in didSelectRowAtIndexPath.
How could I do this? How could I create an array that has both Strings and executable actions associated with each indices? For example:
arrayOneNames = ["Tigris", "Leo", "Barko"]
arrayOneActions = [displayTheTigers, displayTheCats, displayTheDogs]
If "Leo" is selected in the tableView, then "displayTheCats" is executed. Again, I want each array to be a separate Class that I can use as a plugin, so that I can fill the tableView with whichever Class of data I want it to display and execute, depending on which ViewController the user came from previously. Please answer in Swift.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell=UITableViewCell() // cell you've created
cell.data = arrayOneNames[indexPath.row] // passing relevant data
cell.tag = indexPath.row // the tag you want to pass for particular data
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)! as UITableViewCell // take selected cell
if(cell.tag == 0) { // call relevant function accordingly.
self.tigrisTouch()
} else if (cell.tag == 1) {
self.leoTouch()
} else {
self.barkoTouch()
}
}
private func tigrisTouch() {
}
private func leoTouch() {
}
private func barkoTouch() {
}

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.

Sorting rows within a section in a UITableView

I'd like to sort my items within a section of a UITableView. So in the below screenshot under the "N/A" section "Dad" would need to come first followed by "Hi". In the "Last 7d" section the items would also need to be sorted alphabetically.
I can't figure out where in the table's lifecycle to sort the items. Sorting my model (the list of items) and using it with the UITableView doesn't seem to help.
EDIT
Please find below my code. I now understand that the best thing to do is to not only sort but also filter my items into separate lists, one for each displayed section. This is better from a performance perspective, since tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) keeps getting called for each displayed item. Eventhough it doesn't get called for the overall total number of items, it would still be sloppy to sort over and over again the master list of items each time a new item is displayed.
My concern is that the number of sections can vary dynamically and it could reach quite a high number (up to 8). The grouping into sections can also vary - either sections by priority or by due date. So, I think I need to create a more complex data structure for my model -- instead of the current simple list and functions that filter it and sort it at "display time" a class with multiple sorted lists, one for each section, maybe. I will need to create several of these classes, depending on what the user groups by. And these classes will also need to have custom functions to find in the model the item that was tapped / updated based on the indexPath. Makes sense though.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Since I register the TableViewCell class to be used to create a new table view cell in viewDidLoad(), when tableView:cellForRowAtIndexPath: next needs a table cell, your new class will be used automatically.
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCell
// special case for empty, "add reminder" cell
cell.toDoItem = nil
if (indexPath as NSIndexPath).section >= ReminderDue.sections.count {
return cell
}
// If a proper reminder is chosen
if let definiteList = self.groupByDelegator.getReminderList() {
// Take the master list of items and return a filtered sublist of items that belong to that section
let sectionItems = ReminderDue.getReminders(definiteList, inSection: ReminderDue.sections[(indexPath as NSIndexPath).section])
// Do the sorting here
let QUERY_SETTINGS_KEY = "querysettings"
if let definiteIndex = reminderListController.indexOfDisplayedCalInMenu {
let definiteSetting = loadQuerySetting(definiteIndex, saveKey: QUERY_SETTINGS_KEY)
sectionItems.sortBy(definiteSetting.sortBy)
}
cell.selectionStyle = .none // This gets rid of the highlighting that happens when you select a table cell.
// If a reminder (i.e., not the empty row at the end.)
if !sectionItems.list.isEmpty && (indexPath as NSIndexPath).row < sectionItems.count {
// cell.delegate = self
cell.delegate = updateReminderDelegate // Assign as the TableViewCell's delegate the ReminderMainVC (via its UpdateReminderDelegate)
let rem = sectionItems.list[(indexPath as NSIndexPath).row]
cell.toDoItem = rem
}
}
return cell
}
You haven’t shared your code so I can’t use your specific values, but here is an example. I’m assuming you have an array of objects that is used to populate the tableview and that these objects have a property for their section ("low" or "none" priority in your case) and one for their visible name value ("Vic’s" or "Dad" as examples in your case).
Edit: With thanks to #rmaddy
You should sort the array first by the name property and then filter it into a new array for each section. Because this is a stable sorting algorithm, the alphabetic order of the names will persist. Do this when you Load your data, which might be in viewDidLoad():
override viewDidLoad() {
super.viewDidLoad()
// mainArray is your array of data model objects. You will need a strong reference to it and the section arrays so declare them at the top of your view controller class.
// This will sort your array by name
mainArray.sort() { $0.name < $1.name }
// Setup section arrays (I have assumed your objects have a property called priority
// No priority
noPriorityArray = mainArray.filter() { $0.priority == "none" }
// Low priority
lowPriorityArray = mainArray.filter() { $0.priority == "low" }
}
Then access these arrays in the tableview delegate method:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell... // Put your code for getting cell here
// Now code to get cell for each section
if indexPath.section == 0 {
// No priority
cell.textLabel.text = noPriorityArray[indexPath.row]?.name
} else if indexPath.section == 1 {
// Low priority
cell.textLabel.text = lowPriorityArray[indexPath.row]?.name
}
// Other code for other sections or other setup etc.
return cell
}
let sortedItems = arrayOfItems.sorted(by: <)

How to use a string' value as an object name?

I have many objects and I need to use one of them depending on whatever the user pressed to fill my table view. for reducing the use of if statements I think that maybe storing the name of the needed object on a string variable will help. and when the need of filling the table view the string variable will be used instead of check which the approbate object name.
// objects defined here from different classes.
var currentSection : String
#IBAction func button(_ sender: UIButton) {
if sender.tag == 1 { //
currentSection = "object1"
}
else if sender.tag == 2 { //
currentSection = "object2"
}
.........etc
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return(currentSection.item.count)
}
this is what I had done but it didn't work, error: "Value of type 'String' has no member 'item'"
anyone please telling me how to tell the compiler to use the string' value as an object name?
Since currentSection is a string, it doesn't know anything about objects, so currentSection.item is meaningless. Instead, what you could do is use a dictionary that associates string keys to values that represent the data in your section.
struct RowItem {} // the thing that represents the text for your table view row
class MyViewController {
var sectionData: [String: [RowItem]] = [:] // a dictionary that associates string keys to arrays of RowItem
var currentSection: String = ""
func tableView(_ tableView, numberOfRowsInSection section: Int) -> Int {
let thisSection = sectionData[currentSection] ?? [] // you might not have data in the dictionary for currentSection
return thisSection.count
}
}
Update
What is the type that you are using to back your table view cells? In cellForRow you have to configure a cell with some text, images, etc. — whatever the type is of the object that you're using for this (I used RowItem above) should be the type in your dictionary -- [String: [MyRowTypeWhateverThatIs]]. So each string key maps to a value that's an array of data. The items in this array will correspond to the cells in your table view.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let dataForKey: [RowItemOrWhatever] = sectionData[currentSection] ?? [] // this array contains items that correspond 1:1 to the cells in your table view
if indexPath.row >= dataForKey.count { return UITableViewCell() } // it's possible that this index could be outside the bounds of dataForKey, so we need to handle that case sensibly
let cell = tableView.dequeueReusableCell(withIdentifier: reusableCellIdentifier, for: indexPath)
let item = dataForKey[indexPath.row]
cell.textLabel?.text = item.text // or whatever it is that you're doing with these pieces of data
return cell
}
That way you can use the count of the array in numberOfRows and index into the array to get the value for a specific row in cellForRow. As far as IndexPath goes, you only want to use that to index into your own data in cellForRow -- you shouldn't need to store index paths. You will want to define RowItem in such a way that it will have the data you need to configure your cell. So if my cell has some main text, some detail text, and an image, i would define RowItem like this:
struct RowItem {
var mainText: String
var detailText: String
var image: UIImage
}

Swift TableView with multiple prototype cells are not displaying all Rows

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: