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.
Related
GitHub link: https://github.com/Cellomaster87/Storm-Viewer-/tree/challenge12-1a.
Above is complete code but i have noticed many of us that have been using this course for learning doesn't really understand what is happening.
Firstly we needed a dictionary to store the times each pictures would have been viewed I created one like this:
var pictDict = [String: Int]()
Then we modified the cellForRowAt method to include a phrase that would let the user know how many times a picture would have been seen. But in picDict we only have [picture]! why?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Picture", for: indexPath)
let picture = pictures[indexPath.row]
cell.textLabel?.text = picture
cell.detailTextLabel?.text = "Viewed \(pictDict[picture]!) times."
return cell
}
We then added this pictDict[item] = 0 to the loadPictures method to set the viewed times to 0 for every single picture.So Swift knows that 0 should be assigned as a value?
#objc func loadPictures() {
let fm = FileManager.default
let path = Bundle.main.resourcePath!
let items = try! fm.contentsOfDirectory(atPath: path)
for item in items {
if item.hasPrefix("nssl") {
pictures.append(item)
picDict[item] = 0//set the viewed times to 0 for every single picture.
}
4.Inside didSelectRowAt we added this after the if let statement. But does it mean that any value of Int type we put inside [] swift knows that it refers to value from its dictionary
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController{
vc.selectedImage = pictures[indexPath.row]
vc.selectedPictureNumber = indexPath.row + 1
vc.totalPictures = pictures.count
navigationController?.pushViewController(vc, animated: true)
}
let picture = pictures[indexPath.row]
picDict[picture]! += 1
save()
tableView.reloadData()
}
I was using help of Michele Galvagno site :https://artisticscoreengraving.wordpress.com/2019/03/25/hacking-with-swift-challenge-12/ for making this question
The dictionary contains key-value pairs. It's something like a lookup table. The String key represents a picture name of the data source array and the Int value represents the amount of views.
pictDict[picture]! is the syntax for key subscription and means fetch the value for the current picture. It must be unwrapped because a dictionary value is being returned as an optional.
items is the array of (file) names. In the loop the dictionary is populated with the names as keys and with zero values.
In didSelectRow the picture name is taken from the data source array by the given index path (let picture = pictures[indexPath.row]) and the corresponding value in the dictionary is incremented (picDict[picture]! += 1).
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
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
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: <)
I'm getting back to using CloudKit and in the project I'm revisiting, I have a query fetch performed and I am left with an array of CKRecords. I'm setting this array to be displayed via TableController. Anyways, I have this one line of code (which works)... but I am just unsure why I am setting the indexPath as NSIndexPath.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "dining") as! table1cell
let restaurant: CKRecord = categories[(indexPath as NSIndexPath).row]
cell.Name.text = restaurant.value(forKey: "Name") as? String
return cell
With my other non-CKRecord TableController projects, I know I don't have to set the indexPath to itself, in essence. What am I missing here?
The use of the cast to NSIndexPath is pointless. Simply change the line to:
let restaurant: CKRecord = categories[indexPath.row]
If indexPath is not stored in an explicitly typed var, you probably casted it to avoid a compiler message. If the compiler does not know indexPath is of type NSIndexPath, accessing the .row property would likely cause an error.
Where are you declaring/storing indexPath? What happens when you remove the as NSIndexPath cast?
Edit: re-reading your question, I believe the answer is:
"You are not storing indexPath as itself, you are casting whatever is stored in indexPath to be of type NSIndexPath"