I have a UITableView which displays a list of strings from an array. The array is in alphabetical order, and at the top of the UITableView is a UISearchController bar. Everything is working fine. However, I need to make a modification to the list that is presented in the UITableView where a certain subset within the collection is presented at the top of the UITableView (and this subset should ALSO be in alphabetical order). However, once the user enters a character inside the search filter at the top, the list of strings displayed shouldn't matter.
For example, I have a UITableView which displays a list of employees in alphabetical order. What I want to do is when the UITableView loads, instead of presenting ALL the employees in alphabetical order, I would like to first list the managers in alphabetical order, and then the remaining employees in alphabetical order (i.e. AFTER the managers).
The array being received in the ViewController which holds the UITableView from the previous ViewController, which sends this list already sorted in alphabetical order. In other words, the array to begin with, is received already sorted.
I'm assuming you don't want to use sections? That you just want them to all in the same section?
If that's the case you would probably need to do some preprocessing to split the array into your subsets (in viewDidLoad or somewhere else at the beginning of your controller's life cycle):
self.managerArray = [AnyObject]() // you'll need a property to hold onto this new data source
self.employeeArray = [AnyObject]()
for employee: Employee in self.allEmployees {
// assume allEmployees is the alphabetical array
if employee.isManager {
// your condition for determining the subset
managerArray.append(employee)
}
else {
employeeArray.append(employee)
}
}
Because the array is already alphabetized this should populate the subarrays in alphabetical order as well (append just adds to the next index).
Then to make sure the table doesn't load the values before it's been processed in this way you'd need to have
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.employeeArray && self.managerArray {
return self.employeeArray.count + self.managerArray.count
}
return 0
}
Then you can just just populate cells from self.managerArray before moving onto self.employeeArray.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row < self.managerArray.count {
var manager: Employee = self.managerArray[indexPath.row]
// populate your cell info here
}
else {
// else we've already filled in manager's time to start on employees
// subtract the number of managers from the indexPath to start at beginning of employeeArray
var employee: Employee = self.employeeArray[indexPath.row - self.managerArray.count]
// populate cell here
}
return cell
}
Then when you search, you can search on the original self.allEmployees array like normal.
Related
I have a tableview that the user edits information in using textfields, and I store that information into an array that keeps track of all the values. The issue occurs when the user scrolls back to a cell they already edited and the values the added previously are now values from other cells.
I understand that cells are reused, and as a result their data needs to be updated whenever they are being viewed again. I also learned that cellforrowat is called every time a cell is loaded into the view as opposed to just the first time a cell is created. I made a test project to figure out my problem.
My first attempt at solving the problem went like so
cellforrowat is called
if this is the first time the cell is being made set default values and add its data to the array keeping our cell data
If this is not the first time, draw information from the data source at indexpath.row and apply it to the cell
if cellInformation.count < (indexPath.row + 1) // Newly made cell
{
cell.value = 0
cell.tField.text = ""
cellInformation[cellInformation.count] = cell
}
else if (cellInformation.count >= indexPath.row) // Cell we've seen before
{
cell.configure(Value: cellInformation[indexPath.row]!.value) // Sets the textField.text to be the same as the cells value
}
This worked better but when I scrolled back to the top of my tableview, the top most cells were still getting random data. My next attempt generated an ID tag for each cell, and then checking if the id tag of the cell at cellforrowat matched any of the one's in the array.
if cellInformation.count < (indexPath.row + 1) // 0 < 1
{
cell.idTag = idTagCounter
idTagCounter += 1
cell.value = 0
cell.tField.text = ""
cellInformation[cellInformation.count] = cell
}
else if (cellInformation.count >= indexPath.row)
{
for i in 0...idTagCounter - 1
{
if(cell.idTag == cellInformation[i]?.idTag)
{
cell.configure(Value: cellInformation[i]!.value)
}
}
cell.configure(Value: cellInformation[indexPath.row]!.value)
}
This got pretty much the same results as before. When I debugged my program, I realized that when i scroll down my tableview for the first time, indexPath.row jumps from a value like 7 down to 2 and as I scroll more and more, the row goes further away from what it should be for that cell until it eventually stops at 0 even if there are more cells i can scroll to. Once the row hits 0, cellforrowat stops being called.
Any ideas on how i can accurately assign a cells values to the information in my array?
Your premise is wrong:
cellforrowat is called
if this is the first time the cell is being made set default values and add its data to the array keeping our cell data
If this is not the first time, draw information from the data source at indexpath.row and apply it to the cell
You should set up a model object that contains the data for the entries in your table view, and your cellForRowAt() method should fetch the entry for the requested IndexPath.
Your model can be as simple as an array of structs, with one struct for each entry in your table. If you use a sectioned table view you might want an array of arrays (with the outer array containing sections, and the inner arrays containing the entries for each section.)
You should not be building your model (array) in calls to cellForRowAt().
You also should not, not NOT be storing cells into your model. You should store the data that you display in your cells (text strings, images, etc. Whatever is appropriate for your table view.)
Assume that cellForRowAt() can request cells in any order, and ask for the same cells more than once.
Say we want to display an array of animals, and a numeric age:
struct Animal {
let species: String
let age: Int
}
//Create an array to hold our model data, and populate it with sample data
var animals: [Animal] = [
Animal(species: "Lion", age: 3),
Animal(species: "Tiger", age: 7),
Animal(species: "Bear", age: 4)
]
//...
func cellForRow(at indexPath: IndexPath) -> UITableViewCell? {
let cell = dequeueReusableCell(withIdentifier: "cell" for: indexPath)
let thisAnimal = animals[indexPath.row]
cell.textLabel.text = "\(thisAnimal.species). Age = \(thisAnimal.species)"
}
Note that for modern code (iOS >=14), you should really be using UIListContentConfigurations to configure and build your cells.
I am trying to use a UITableview with multiple selection on and a check mark accessory view for selected rows. This is mostly working if I turn on and off the accessory view in tableView:didSelectRow.
However, I tried to build a selectAll method, and I found that the array of selected cells was being cleared after I had spun through all the cells and selected them if I then call reloadData().
I suspect reloading the table clears selection. I don't know of any other way to have all the cells drawn after I set the selected flag and accessory view.
I am wondering if I need to keep my own array of selected rows. Has anyone else built something like this? Its seems like a common scenario.
Any tips or sample code appreciated.
Take an Array and add the indexPath of each selected cell into it and put a condition in cellForRowAt... that if the Array contains that particular indexPath, set it as selected.
There are two approaches you can take. One is to track the selected row numbers. To do this, you can use an NSMutableIndexSet or its Swift counterpart IndexSet.
Essentially, when a row is selected, add it to the set. When you deselect it, remove it from the set. In cellForRowAtIndexPath you can use containsIndex to determine if a check mark should be shown or not.
Since you explicitly mention an issue with selection when you reload the table, it is worth considering the issue with storing row numbers (whether in a set or an array), and that is that row numbers can change.
Say I have selected rows 4,7 and 9 and these values are stored in the index set. When I reload the data, new data may have been inserted after the "old" row 8, so now I should be selecting rows 4,7 and 10, but I will be selecting 4,7 and 9 still.
A solution to this is to store some sort of unique identifier for the data that should be selected. This will depend on your data, but say you have a string that is unique for each item. You can store this string in a NSMutableSet or Swift Set, which again makes it easy to check if a given item is selected using contains
you need add some functionality in cellForRowAtIndexPath method like this ang your view controller code like this
let we take one example of photo gallery application
class CreateEvent: UIViewController,UITableViewDataSource,UITableViewDelegate {
var yourArray : [Photo] = [Photo]()
//MARK: - Content TableView Methods
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath)
let objPhoto = yourArray[indexPath.row]
if objPhoto.isPhotoSelected == true
{
cell.accessoryType = .Checkmark
}
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
let objPhoto = yourArray[indexPath.row]
objPhoto.isPhotoSelected = true
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell.accessoryType = .Checkmark
}
//MARK: Action Method
func selectAllPhoto()
{
for objPhoto in yourArray
{
objPhoto.isPhotoSelected = true
}
tableView.reloadData()
}
}
and one more thing you need to create your custom object like
class Photo: NSObject {
var photoName:String = ""
var isPhotoSelected = false
}
hope this will help you
The best approach for multiple selection is
Take a model object, in that take all your attributes and one extra boolean attribute (like isSelected) to hold the selection.
In case of selecting a row
Fetch the relevant object from the array
Then update the isSelected boolean (like isSelected = !isSelected) and reload table.
In case of select all case
Just loop through the array.
Fetch the model object from array.
make the isSelected = true.
After completion of loop, reload the table.
I've got an array of rooms I'm wanting to filter by usercount/topvotes sorta stuff.
I'm hitting buttons to trigger the filter changes and set a value to "CurrentSearch", reloading my tabledata, and in my cellForRowAtIndexPath I'm checking the current currentsearch value and filtering the list accordingly.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if currentSearch == "new" {
self.PersonalSearchesList = PersonalSearchesList.sorted{ $0.users < $1.users }
} else if currentSearch == "top" {
self.PersonalSearchesList = self.PersonalSearchesList.sorted{ $0.latestUser < $1.latestUser }
}
My images/labels load in perfectly fine before the filtering. On the initial load I'm setting currentSearch to "new" and it works perfectly. It's only once I start trying to swap between filters that things get funky. I'm getting random duplicates of cells, and while I'm swapping between the filters the incorrect loads stay consistent..as in the same duplicates of the same cells are being made/placed at the same spots.
I have it when I click on a cell the information for the cell is printed..and despite the cell information being loaded incorrectly the actual information is correct and the lists are ordered as they should be.
any ideas?
You should do something like this:
var currentSearch: String {
didSet {
// sort your array
tableView?.reloadData() // reload data
}
}
if you will sort your every then it every time get sorted when you scroll your tableview or reload it. so sort your data somewhere else in other method and after successfully sorting reload your tableview.
I have a table view of tasks. I have also a segmented control above the table that has two choice, the first is for displaying the regular table the other one filters the array of the table to pick the elements that has the same date of the current date and then , I add these new elements to a new array and reload the table view that checks if the selected button in the segmented control is the second one , it will load the elements of filtered array as cells of the table. I have an action on the cell, so if you click them you go to a view where you can change the color of the cell. The problem is happening here. I store the index path of the cell in the table to determine which cell ( row ) is going to be edited, but if the selected segmented button is the second one, the index will be stored according to the order of the element in the filtered table , so when I finish editing the cell, the color will be changed of the cell that has that index not the actual cell because of the conflict between the normal table and the filtered table indexes. Hope that clear enough to explain my case , if it is not, ask me for more explanation.
here is the table loading code :
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
if sortTableClicked
{
return filteredAchivements.count
}else
{
return achievements.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("myCell2", forIndexPath: indexPath) as! AchievementsCells
var arr = [achievement]()
if sortTableClicked
{
arr = filteredAchivements
}else
{
arr = achievements
}
cell.name.text = arr[indexPath.row].name
cell.des.text = arr[indexPath.row].des
cell.date.text = "\(arr[indexPath.row].date) : تم إضافته بتاريخ "
cell.backgroundColor = arr[indexPath.row].cellColor
return cell
}
and I have this function that receives the index path sent from the table and apply the changes depending on this index path.
func applyEdit(color: UIColor, name: String, des: String, index: NSIndexPath) {
taskMgr.editCellDataAtIndex(index, forTable: "achievementsTable", theNewName: name, theNewDes: des, theNewColor: color)
taskMgr.loadData("achievementsTable")
achievements = []
achievements = taskMgr.achievements
tableView.reloadData()
}
what happens is that if the loaded table is the filtered one and let's say I want to edit the second cell , the indexpath.row will be 1 , so the changed- color cell will be the second one on the table , but actually the second cell in the filtered table is the fifth one in the non-filtered table , so I want to apply changes on this fifth one not the second one , does anyone have any idea ?
Problem is that you are saving cell index path which is static and is not changing with change in your table view.
You can fix this easily by adding one more bool property in your model arr that feeds data to table. Name is shouldChangeColor and set this property to true. Now, your model data is updated. Use this property in your cellForRowAtIndexPath function to change the cell colour.
Since cells are drawn in the order you supply your data from model, this would guarantee that your 2nd cell is painted in colour in filtered mode and 5th in other tab mode.
I have a UITableView with a segue that connects to a view controller. When the user taps on a cell, I would like the indexPath.row to be passed to that view controller so I can update the content in the view based on which cell has been tapped.
I am aware of the different methods of doing this, however I would like to use NSUserDefaults() for my particular use. The problem is that I don't know how to convert the indexPath.row into an integer to be saved into NSUserDefaults(). This is what I have tried:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
println(indexPath.row)
NSUserDefaults.standardUserDefaults().setObject(NSInteger(indexPath.row), forKey: "index")
self.performSegueWithIdentifier("event", sender: self)
}
Second View Controller:
override func viewDidAppear(animated: Bool) {
println(NSUserDefaults.standardUserDefaults().integerForKey("event"))
}
At the moment, when I try to print out the indexPath.row in the next view controller, it always just returns 0.
You aren't using the same key
NSUserDefaults.standardUserDefaults().setObject(NSInteger(indexPath.row), forKey: "index") // <--- INDEX
NSUserDefaults.standardUserDefaults().integerForKey("event")) // <-- EVENT
Also you :
Might want to save it / read it with the equivalent method setInteger:forKey: and integerForKey:
Don't have to convert the value (it's already an integer) ;
Make sure you don't paint yourself in a corner. An NSIndexPath contains two numbers, not one: A section and a row within that section. In trivial cases when a table view has only one section the section number will always be zero, but you will get more complicated cases.
If you want to store index paths, I would add a category to NSUserDefaults with one method that turns an NSIndexPath into a dictionary with keys section and row and writes that, and one that reads a dictionary and creates an NSIndexPath with the values for section and row.
That said, a row number is something horribly unstable. Your UI designers decide that two rows have to change their order, and all your preferences are broken. What I do is one enum that identifies the purpose of each row; that enum must never, ever, ever change its values, and that is stored in the preference. And then there is another enum that refers to items in the UI, and that can change freely.
Saving A Row(integer) Value :
NSUserDefaults.standardUserDefaults().setInteger(indexPath.row, forKey: "row")
NSUserDefaults.standardUserDefaults().synchronize()
Loading A Row(integer) Value:
row() -> Int
var row = NSUserDefaults.standardUserDefaults().integerForKey("row")