Sorting rows within a section in a UITableView - swift

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: <)

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() {
}

Realm results object with incorrect results (iOS, Swift)

I am having issues getting Realm's result object to be accessed correctly using UITableView's cellForRowAt.
Here's the setup:
UITableViewController is divided into sections based on the Objects's category (a string defined in the object).
UITableViewController has a segue to a UIViewController which takes form input. That view controller writes to Realm and then makes a call back via delegation to refresh the table view data.
When that screen dismisses and returns to the UITableViewController, as I try to add the row via category, I am getting empty objects. However, when I use a for loop within cellForRowAt, I can access the data.
Here is what I'm running in this section:
func loadItems() {
itemsList = try! Realm().objects(Items.self).filter("list_id = \(list_id)").sorted(byKeyPath: "item_category")
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemListCell", for: indexPath)
let categoryName = categories.categories[indexPath.section]
let currItem = itemsList[indexPath.row]
if currItem.item_category == categoryName {
cell.textLabel!.text = currItem.item_name
}
return cell
}
It seems to be evaluating category correctly and stepping into that block, but the object's item_name and item_category is null. Here is a screenshot of the debugger inside the if statement:
Debugger Image
Is there something I need to change with how I'm using the object, pulling data, etc, to get the data correct in the object?
Found my answer here: UITableView with Multiple Sections using Realm and Swift
This is the change I made to cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemListCell", for: indexPath)
let currItem = itemsList.filter("item_category = '\(categories[indexPath.section])'")[indexPath.row]
cell.textLabel!.text = currItem.item_name
return cell
}
The issue I was having was I was constantly pulling the first position results object and not the first position within a section. I needed to narrow down to my section and then pull the first row.
I think it may have to do with your filters predicate. Try changing
itemsList = try! Realm().objects(Items.self).filter("list_id = \(list_id)").sorted(byKeyPath: "item_category"
to this
itemsList = try! Realm().objects(Items.self).filter("list_id = '\(list_id)'").sorted(byKeyPath: "item_category"
Add single quotations around '(list_id)'.
Realm Filtering

Table view cell get initial value after scrolling

I have a table view and 10 different prototype cell. I used storyboard and created custom UITableCell class for each cell.
There is a checkbox in CheckBoxCell. I created these checkboxes in loop according to options count.
Problem is that after I checked a checkbox, checkbox value changes but when I scroll up or down the table view, checkbox value changes with inital value.
I investigated some questions in stackoverflow. I live this problem, because of after every scroll dequeReusebleCell works and re-create the cell in the queue. I tried to use these solutions, but I cannot succeeded.
I am new for Swift and I don't know how can I solve this problem.
May someone tell me how can I solve this problem and what is the correct approach?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let object = surveyDetailArray.first!.elements[indexPath.row]
switch object.type {
case CellConfig.checkbox.rawValue:
let cell = tableView.dequeueReusableCell(withIdentifier: "CheckboxCell", for: indexPath) as! CheckboxCell
let object = surveyDetailArray.first!.elements[indexPath.row]
for label in object.options {
cell.checkbox = BEMCheckBox()
cell.checkbox.onAnimationType = .bounce
cell.checkbox.offAnimationType = .bounce
cell.checkbox.boxType = .square
cell.checkbox.onFillColor = .red
cell.checkbox.offFillColor = .white
cell.checkbox.onCheckColor = .white
cell.checkbox.delegate = self
cell.checkbox.tag = label.id
cell.contentView.addSubview(cell.checkbox)
}
return cell
}
In your problem the cell is dequeuing and is getting back to the old state, that's will obviously happen because of dequeReusableCell,
now for the solution, you should use a model to store the states of different checkboxes and on cellforRowAt
add the code for checkbox persistance according to the model, when you enable a checkbox,
change the value of the variable in the model also and leave rest it on your cellForRowAt code. I'm adding a small example for your understanding, hope it helps.
CODE
Struct ButtonsStates {
var isButtonEnabled : Bool = false
}
// In your ViewController use the above model for saving buttonValues
var buttonStates : [ButtonStates]? // initialize as many as the rows
// in cellForRowAt
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ExampleCell", for: indexPath) as? ExampleCell else { return fatalError("") }
// here if the cell is dequeued still when cell will again be visible then this condition will be checked
cell.customButton.isSelected = buttonStates[indexPath.row].isButtonEnabled
return cell

Capturing multiple values from UITableView with Swift

I'm trying to use a uitableview as a picker to allow the user to make multiple selections. I've set the tableview up to allow this and my coreData has a one-to-many relationship to allow for multiple values. My problem is that I cannot figure out how to capture the selected rows as values in my coreData.
I populate the table by fetching data from my 'Players' entity and storing it in this variable:
var playerPickerData = [NSManagedObject]()
I am guessing that I need to do something with the selectedrowsatindexpaths function to then extract the values back out of playerPickerData to be able to then add them to my 'Match' entity (which is linked to my players entity with a one-to-many relationship).
The code I have to populate my table cells is:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = playersPicker.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.accessoryType = cell.isSelected ? .checkmark : .none
cell.selectionStyle = .none
let fName = playerPickerData[indexPath.row].value(forKey: "firstName") as! String
let lName = playerPickerData[indexPath.row].value(forKey: "lastName") as! String
let playersNameData = "\(fName) \(lName)"
cell.textLabel?.text = playersNameData
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
selectedIndexPaths.append(indexPath as NSIndexPath)
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .none
}
I have a UIPickerView that is used to select one value ('Game Name') and I can capture what I need to from this without issue by using the following on my SavePressed function:
let newMatch = NSEntityDescription.insertNewObject(forEntityName: "Match", into: context)
let selectedGame = pickerData[gamePicker.selectedRow(inComponent: 0)]
newMatch.setValue(selectedGame, forKey: "game")
I just need to know how i can save multiple values forKey = "players". Any advice on how/where to do this in my code would be very much appreciated.
If I can provide any more information then please let me know.
I genuinely cannot find anything that answers these questions here or elsewhere online. I think this might relate but I cannot translate it to my needs: Saving multiple values from a UITableView
I just need to know how i can save multiple values forKey = "players".
Always read the documentation of methods you are using when questions arise. The documentation of NSManagedObject.setValue(:forKey:) states:
If key identifies a to-one relationship, relates the object specified by value to the receiver, unrelating the previously related object if there was one. Given a collection object and a key that identifies a to-many relationship, relates the objects contained in the collection to the receiver, unrelating previously related objects if there were any.
So as long as you can construct and maintain a Set (Core Data uses sets to store to-many relationships) of your selected NSManagedObjects, you can simply do something along the lines of:
selectedObjects = [NSManagedObject]() // Fill this Array
parent.setValue(Set(selectedObjects), forKey: "key")
and it will automatically reset the to-many relationship, adding any new items and subtracting any that are not contained in the new collection.
It looks like you are maintaining a selectedIndexPaths array. In that case, you could do:
selectedIndexPaths.map { playerPickerData[$0.row] }
to get the selected objects, assuming your table view has a single section. You'll then need to turn that into a Set as displayed above. You could also collect the selected index paths as a set in the first place, but then after you map it it will be an Array again anyway.
PS
Note that in your didSelectRow you add the selected index paths to selectedIndexPaths, but you never remove them from selectedIndexPaths in didDeselectRow.

Add a cell only if certain conditions are met

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.