I am using a UITableViewController. The UITableView is presenting 11 custom Cells.
10 of those are dynamic, one is static.
Nevertheless counting the rows either with:
let cells = self.tableView.visibleCells as? [chatUebersicht]
or
for (var row = 0; row < gruppenNamen.count; row++) {
if let cell:chatUebersichtCell = table.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: 0)) as? chatUebersichtCell {
cells.append(cell)
}
}
is returning the correct number. It is returning the number 5 each time.
I think there is an issue with preloading the cells. is there a way to overcome this?
visibleCells only returns the reusable cells you can currently see. That's the beauty of a UITableView - There isn't an instance of a cell for every row of data.
You should reevaluate what you are trying to do and determine whether it's a presentational operation or something that can be done by working with your array of data that feeds the tableview's data source.
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.
func convertPointToIndexPath(_ point: CGPoint) -> (UITableView, IndexPath)? {
if let tableView = [tableView1, tableView2, tableView3].filter({ $0.frame.contains(point) }).first {
let localPoint = scrollView.convert(point, to: tableView)
let lastRowIndex = focus?.0 === tableView ? tableView.numberOfRows(inSection: 0) - 1 : tableView.numberOfRows(inSection: 0)
let indexPath = tableView.indexPathForRow(at: localPoint) ?? IndexPath(row: lastRowIndex, section: 0)
return (tableView, indexPath)
}
return nil
}
So i got this method, which converts a CGPoint into the indexPath of the given uitableView. I struggle with the filter-Method on the array which contains uitableViews.
I got an array outside of this method which contains any number of uitableViews. For example:
public var littleKanbanColumnsAsTableViews: [UITableView] = []
So i got to make a change inside of the method. Like this:
if let tableView = littleKanbanColumnsAsTableViews.filter({ $0.frame.contains(point)}).first { ... }
Now when i click on any tableView on the gui, i track the coordinates of the point and transform it on the belonging tableview with the frame.contains(point) method.
My problem is that the filter is not working, it always gives me the first tableView back, no matter which tableview is clicked. Why it doesn't work with my littleKanbanColumnsAsTableViews-Array?
One hint:
let tableView = littleKanbanView.littleKanbanColumnsAsTableViews[3]
When i indexing its direct then it works. But i want it depending on which tableView is containing the clicked point.
Here is my array with the tableViews, in this case the array contains 5 tableviews.
array containing tableviews
Now i want to filter the tableView out of them, which includes the point from tapping on this tableView. How can i achieve this?
For more understanding, i add the ui, here is it:
UI of my app
When i click on this tableView it works, because it is the first element in my array of tableViews. So for this case the convertPointToIndexPath-Method is working.
But when i scroll horizontally to the second tableView for example and click on that, it doesn't work. Because I think the method gives me always the first element back, but i thought it filters it with the given condition.
What is the problem, why doesn't work the condition{ $0.frame.contains(point)}? It have to localize the tableView when the coordinates of the point are tracked.
Preferred Solution:
In this case the moment the first satisfying condition is met, the rest of the elements are not traversed.
if let tableView = littleKanbanColumnsAsTableViews.first(where: { $0.frame.contains(point) }) {
}
Not so efficient solution:
In this case all the elements in the array are traversed to build an array of table views that satisfy the condition. Then the first element of that filtered array is chosen.
if let tableView = (littleKanbanColumnsAsTableViews.filter{ $0.frame.contains(point)}).first {
}
This question already has answers here:
How to detect the end of loading of UITableView
(22 answers)
Closed 6 years ago.
I need to call a function after the UITableView has been loaded completely. I know that in most cases not every row is displayed when you load the view for the first time, but in my case, it does as I only have 8 rows in total.
The annoying part is that the called function needs to access some of the tableView data, therefore, I cannot call it before the table has been loaded completely otherwise I'll get a bunch of errors.
Calling my function in the viewDidAppear hurts the user Experience as this function changes the UI. Putting it in the viewWillAppear screws up the execution (and I have no idea why) and putting it in the viewDidLayoutSubviews works really well but as it's getting called every time the layout changes I'm afraid of some bugs that could occur while it reloads the tableView.
I've found very little help about this topic. Tried few things I found here but it didn't work unfortunately as it seems a little bit outdated. The possible duplicate post's solution doesn't work and I tried it before posting here.
Any help is appreciated! Thanks.
Edit: I'm populating my tableView with some data and I have no problems with that. I got 2 sections and in each 4 rows. By default the user only sees 5 rows (4 in the first section, and only one in the second the rest is hidden). When the user clicks on the first row of the first section it displays the first row of the second section. When he clicks on the second row of the first section it displays two rows of the second section, and so on. If the user then clicks on the first row of the first section again, only one cell in the second section is displayed. He can then save his choice.
At the same time, the system changes the color of the selected row in the first section so the users know what to do.
Part of my issue here is that I want to update the Model in my database. If the users want to modify the record then I need to associate the value stored in my database with the ViewController. So for example, if he picked up the option 2 back then, I need to make sure the second row in the first section has a different color, and that two rows in the second sections are displayed when he tries to access the view.
Here's some code :
func setNonSelectedCellColor(indexPath: NSIndexPath) {
let currentCell = tableView.cellForRowAtIndexPath(indexPath)
currentCell?.textLabel?.textColor = UIColor.tintColor()
for var nbr = 0; nbr <= 3; nbr++ {
let aCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: nbr, inSection: 0))
let aCellIndexPath = tableView.indexPathForCell(aCell!)
if aCellIndexPath?.row != indexPath.row {
aCell?.textLabel?.textColor = UIColor.blackColor()
}
}
}
func hideAndDisplayPriseCell(numberToDisplay: Int, hideStartIndex: Int) {
for var x = 1; x < numberToDisplay; x++ {
let priseCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: x, inSection: 1))
priseCell?.hidden = false
}
if hideStartIndex != 0 {
for var y = hideStartIndex; y <= 3; y++ {
let yCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: y, inSection: 1))
yCell?.hidden = true
}
}
}
These two functions are getting called every time the user touches a row :
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let path = (indexPath.section, indexPath.row)
switch path {
case(0,0):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(1, hideStartIndex: 1)
data["frequencyType"] = Medecine.Frequency.OneTime.rawValue
case(0,1):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(2, hideStartIndex: 2)
data["frequencyType"] = Medecine.Frequency.TwoTime.rawValue
case(0,2):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(3, hideStartIndex: 3)
data["frequencyType"] = Medecine.Frequency.ThreeTime.rawValue
case(0,3):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(4, hideStartIndex: 0)
data["frequencyType"] = Medecine.Frequency.FourTime.rawValue
default:break
}
}
I store the values in a dictionary so I can tackle validation when he saves.
I'd like the first two functions to be called right after my tableView has finished loading. For example, I can't ask the data source to show/hide 1 or more rows when I initialize the first row because those are not created yet.
As I said this works almost as intended if those functions are called in the viewDidAppear because it doesn't select the row immediately nor does it show the appropriate number of rows in the second sections as soon as possible. I have to wait for 1-2s before it does.
If you have the data already that is used to populate the tableView then can't you use that data itself in the function? I am presuming that the data is in the form of an array of objects which you are displaying in the table view. So you already have access to that data and could use it in the function.
But if that's not the case then and if your table view has only 8 rows then you can try implementing this function and inside that check the indexPath.row == 7 (8th row which is the last one).
tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
Since all your rows are visible in one screen itself without scrolling you could use this function to determine that all the cells have been loaded and then call your function.
Using Swift, how can I iterate over all the UITableCells given a section id (eg: all cells in section 2)?
I only see this method: tableView.cellForRowAtIndexPath, which returns 1 cell given the absolute index, so it doesn't help.
Is there an elegant and easy way?
Note: I want to set the AccesoryType to None for all of the cells in a section, programatically, say: after a button is clicked, or after something happends (what happends is not relevant for the question)
I have the reference for the UITableView and the index of the section.
You misunderstand how table views work. When you want to change the configuration of cells, you do not modify the cells directly. Instead, you change the data (model) for those cells, and then tell your table view to reload the changed cells.
This is fundamental, and if you are trying to do it another way, it won't work correctly.
You said "I need the array of cells before modifying them…" Same thing applies. You should not store state data in cells. As soon as a user makes a change to a cell you should collect the changes and save it to the model. Cells can scroll off-screen and their settings can be discarded at any time.
#LordZsolt was asking you to show your code because from the questions you're asking it's pretty clear you are going about things the wrong way.
EDIT:
If you are convinced that you need to iterate through the cells in a section then you can ask the table view for the number of rows in the target section, then you can loop from 0 to rows-1, asking the table view for each cell in turn using the UITableView cellForRowAtIndexPath method (which is different than the similarly-named data source method.) That method will give you cells that are currently visible on the screen. You can then make changes to those cells.
Note that this will only give you the cells that are currently on-screen. If there are other cells in your target section that are currently not visible those cells don't currently exist, and if the user scrolls, some of those cells might be created. For this reason you will need to save some sort of state information to your model so that when you set up cells from the target section in your datasource tableView:cellForRowAtIndexPath: method you can set them up correctly.
For Swift 4 I have been using something along the lines of the following and it seems to work pretty well.
for section in 0...self.tableView.numberOfSections - 1 {
for row in 0...self.tableView.numberOfRows(inSection: section) - 1 {
let cell = self.tableView.cellForRow(at: NSIndexPath(row: row, section: section) as IndexPath)
print("Section: \(section) Row: \(row)")
}
}
Im using same way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not
//get section of interest i.e: first section (0)
for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
{
var indexPath = NSIndexPath(forRow: row, inSection: 0)
println("row")
println(row)
//following line of code is for invisible cells
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)
//get cell for current row as my custom cell i.e :roomCell
var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
}
* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible :D
To answer my own question: "how can I iterate over all the UITableCells given a section id?":
To iterate over all the UITableCells of a section section one must use two methods:
tableView.numberOfRowsInSection(section)
tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))
So the iteration goes like this:
// Iterate over all the rows of a section
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
var cell:Cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?
// do something with the cell here.
}
At the end of my question, I also wrote a note: "Note: I want to set the AccesoryType to None for all of the cells in a section, programatically". Notice that this is a note, not the question.
I ended up doing that like this:
// Uncheck everything in section 'section'
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?.accessoryType = UITableViewCellAccessoryType.None
}
If there is a more elegant solution, go ahead and post it.
Note: My table uses static data.
Swift 4
More "swifty", than previous answers. I'm sure this can be done strictly with functional programming. If i had 5 more minutes id do it with .reduce instead. ✌️
func cells(tableView:UITableView) -> [UITableViewCell]{
var cells:[UITableViewCell] = []
(0..<tableView.numberOfSections).indices.forEach { sectionIndex in
(0..<tableView.numberOfRows(inSection: sectionIndex)).indices.forEach { rowIndex in
if let cell:UITableViewCell = tableView.cellForRow(at: IndexPath(row: rowIndex, section: sectionIndex)) {
cells.append(cell)
}
}
}
return cells
}
Im using this way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not
//get section of interest i.e: first section (0)
for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
{
var indexPath = NSIndexPath(forRow: row, inSection: 0)
println("row")
println(row)
//following line of code is for invisible cells
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)
//get cell for current row as my custom cell i.e :roomCell
var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
}
* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible
You can use
reloadSections(_:withRowAnimation:) method of UITableView.
This will reload all the cells in the specified sections by calling cellForRowAtIndexPath(_:). Inside that method, you can do whatever you want to those cells.
In your case, you can apply your logic for setting the appropriate accessory type:
if (self.shouldHideAccessoryViewForCellInSection(indexPath.section)) {
cell.accessoryType = UITableViewCellAccessoryTypeNone
}
I've wrote a simple extension based on Steve's answer. Returns the first cell of given type (if any) in a specified section.
extension UITableView {
func getFirstCell<T: UITableViewCell>(ofType type: T.Type, inSection section: Int = 0) -> T? {
for row in 0 ..< numberOfRows(inSection: section) {
if let cell = cellForRow(at: IndexPath(row: row, section: section)) as? T {
return cell
}
}
return nil
}
}
I want to pass table cell's textLabel data of UITableViewController to NSArray. Those cell have identifier name Cells and accessory type checkmark
Code that does exactly what you asked is here:
func getCellsData() -> [String] {
var dataArray: [String] = []
for section in 0 ..< self.tableView.numberOfSections() {
for row in 0 ..< self.tableView.numberOfRowsInSection(section) {
let indexPath = NSIndexPath(forRow: row, inSection: section)
let cell = self.tableView.cellForRowAtIndexPath(indexPath)!
if cell.reuseIdentifier == "Cells" && cell.accessoryType == UITableViewCellAccessoryType.Checkmark {
dataArray.append(cell.textLabel!.text!)
}
}
}
return dataArray
}
But I would like to recommend you find different approach, because this is the rude traverse of tableView. You probably have your dataSource model that can give you ll data you need. Additionally, this code doesn't check for errors, for example if there is no text in cell at some indexPath
Two things I'd advise.
First, looks like you're using the checkmark accessory to indicate multiple selections. It's really intended for single selection, like a radio button. Better to use the allowMultipleSelectionsoption on the tableview. This will allow...
...the second thing. Copying text from cells into an array is the wrong way round to do it. Better to ask the table view and call it's - (NSArray *)indexPathsForSelectedRows this will give you an array of index paths to selected cells then you can ask for each cell and grab any data you want from it. This gives you better live data and prevents you unknowingly creating a circular reference from the view controller of the tableview and the content in the tableview cells.