Forgive the naive nature of this question, but despite my searching here, I can't quite find a solution.
In my project, I have a UITableView with custom cells, all of which are populated by data from an API. At this point, based on the desired design, four of the cells are visible on-screen at launch, and I have to scroll to see the other four. Each cell has two components; a label and a UIView, which is generating a line chart. The first four cells all work properly, but the second set of cells (which are off-screen at loading) populate the label correct, but the UIView is not showing the correct set of data (it is populating with previously used data).
Oddly, if I tap on one of the problematic cells, the cell immediately reloads with the correct data.
For context, here's part of my TableViewController;
...
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return minions.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return minionCellAtIndexPath(indexPath)
}
func minionCellAtIndexPath(indexPath:NSIndexPath) -> MinionCell {
let minion = minions[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier(minionCellIdentifier, forIndexPath: indexPath) as! MinionCell
if let name = minion.name {
cell.nameLabel?.text = minion.name ?? "Minion"
cell.lineChart?.graphPoints = minion.bandwidth ?? [1,2,3,4,5]
} else {
cell.nameLabel?.text = "No data available."
cell.lineChart?.graphPoints = [1,2,3,4,5]
}
return cell
}
...
Is it possible that I need to somehow "pre-render" the off-screen cells at load? Again, sorry for the naive nature.
As always, thank you for your time!
As you most probably know, cells that go off screen get reused for the new rows. That is why the new rows come with the old graphs. The fact that if you tap the cell the graph refreshes and is correctly displayed makes me think that it is a drawing problem. Try triggering a force redraw by calling setNeedsDisplay on the view with the graph:
cell.lineChart?.graphPoints = [1,2,3,4,5]
cell.lineChart?.setNeedsDisplay()
I am somewhat skeptical about your function minionCellAtIndexPath.
The reason you are getting older data again, and not newer is probably because you are not resetting it.
Before your if let checks, set hardcoded values, the ones that you are setting in your else. and then run again.
Related
I am changing the numberOfLines attribute on a label that lives in a custom UITableViewCell when the cell is tapped. However, this is not reflected in the UI until the second tap. The cell is configured as a prototype cell in the table view to initially have 2 lines.
Interestingly enough, when I print out the numberOfLines value before and after my tapped() function runs, the values start off different, and then synchronize - after the first tap, I see 2 lines before the function runs, then 0 lines after the function runs. However, after subsequent taps, I see the same value before and after my function, which makes it seem like it's not doing anything, even though the UI does stretch and shrink the cell, and the numberOfLines value is changed for the next time the didSelectRowAtIndexPath function runs.
I'm only seeing this behavior with tableView.reloadRows(). If I do a full update with tableView.reloadData(), the cell appropriately grows and collapses the first time it is tapped. However, this feels a bit ham-fisted and doesn't animate nicely like reloadRows() does.
TableView Implementation
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ReviewTableViewCell
else { return }
let data = tableData[indexPath.row]
print("old number of lines: \(cell.detailLabel.numberOfLines)")
//data.isOpen is set to false initially
cell.tapped(data.isOpen)
tableData[indexPath.row].isOpen = !data.isOpen
tableView.reloadRows(at: [indexPath], with: .fade)
print("old number of lines: \(cell.detailLabel.numberOfLines)")
// tableView.reloadData()
}
Custom Table View Cell method
func tapped(_ isOpen: Bool) {
if !isOpen {
detailLabel.numberOfLines = 0 }
else {
detailLabel.numberOfLines = 2 }
}
I am expecting this code to expand the cell once it is reloaded with tableView.reloadRows() if the numberOfLines is set to 0 and collapse the cell when it is set to 2. This does work, but only after tapping the cell two+ times. This should work with the first tap as well.
Here is a link of a gif that shows the issue: https://imgur.com/a/qe2uAXj
Here is a sample project that is similar to what's going on in my app: https://github.com/imattice/CellLabelExample
Just to be clear, to get this trick work UILabel generally must be constrained on each side to it's superview, in this way when it changes its intrinsicContentSize is able to push each side to accomodate the text.
Saying that, try to wrap the tapped method with those two methods:
tableView.beginUpdates()
if !isOpen {
detailLabel.numberOfLines = 0
}
else {
detailLabel.numberOfLines = 2
}
tableView.endUpdates()
Of course tableview must be set to automatic size:
tableView.estimatedRowHeight = <#What you want#>
tableView.rowHeight = UITableView.automaticDimension
I was able to work out what was going on. The problem is in two parts.
The first part is calling reloadRows(). This method is swapping out the cells with a new cell rather than updating the cell that already exists. Therefore, I'm changing the number of lines on that hidden swap cell rather than the cell that is in view. This behavior is mentioned in the docs:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out.
Additionally, I'm using structs as the data model for tracking the open status of the cell. In Swift, structs are copy-on-write, which means that if a value is changed on that struct, a new struct is created rather than changing the value of that struct I'm pointing to. This means the line tableData[indexPath.row].isOpen = !data.isOpen doesn't do anything useful - we look at the tableData struct at the index path, get it's isOpen value, copy a new struct and change that new struct's isOpen value, and then throw it out because the new struct is not assigned anywhere.
The solution is to not use the reloadRows() method and to either use
A) a class for the data object
B) replace the data at indexPath.row to the copied struct
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CustomCell else { return }
var data = tableData[indexPath.row]
tableView.beginUpdates()
cell.tapped(isOpen: data.isOpen)
data.isOpen = !data.isOpen
tableData[indexPath.row] = data
tableView.endUpdates()
}
Current Situation:
I have a UITableView with custom cells. In the cells are 1 label and 1 textview.
Scrolling is enabled for the UITableView.
Below the Table, I have a button to save the entries from the textview.
My Problem is:
I get only the value from the first row.
From the other rows, I get always an empty text, but only if the user has scrolled.
I don't understand why and I hope you can help me.
Here is my Code:
#objc func save()
{
for i in 0..<labels.count
{
let indexPath = IndexPath(item: i, serction: 0)
let cell = self.SearchTable.dequeueReusableCell(withIdentifier: "searchCell",
for: indexPath) as! SearchCell
print("Index: \(indexPath)")
if(cell.EditField.text = ""
{
continue;
}else{
...
}
}
}
Debugger
First Row: FirstRow
Other Rows: SecondRow
Cells in a tableview are reused, so a cell at an index path that is not visible could have values from a completely different cell. If you want to get all the cells that are currently visible on the screen, you could use tableView.visibleCells docs to get all the cell that are currently on screen and extract data from there.
Alternatively, you could choose to not implement cell reuse and make your table view static. You can do this in Interface Builder, or you could also choose to create all the cells up front and return your pre-made cells in tableView(_:cellForRowAt:). Note that a setup like this is okay for small datasets, but has terrible performance for larger sets so be aware that this might not be the best way to do things. It really depends on your situation. This method of doing things would end up looking a bit like this:
var cells = [UITableViewCell]()
override func viewDidLoad() {
// do all kinds of stuff
for field in fields { // or whatever else mechanism you use as your datasource
let cell = UITableViewCell()
// configure your cell
cells.append(cell)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return cells[indexPath.row]
}
The third and last way you might want to solve this is to add a delegate to your cells and set the view controller as the delegate. If the text changes, call the delegate with the cell's index path and the new text value. You can then store this text somewhere in the view controller and read it from there when you save rather than pulling it from the cell's textfield. Personally I would prefer this method.
Rather than iterate over the cells in the tableview, you could just get the data from the data source.
Whenever the text in the cell's text field changes you could update the data source and then use the information from there to perform your save.
You must be having some kind of data source anyway, otherwise what happens to the text when the cell scrolls off the screen and comes back on again? If you aren't storing the text somewhere then you've got nothing to populate the cell with in the table views cellForRow(... delegate method.
My tableview is not scrolling smoothly. I have seen this comment from apple.
But very important thing is still there: tableView:cellForRowAtIndexPath: method, which should be implemented in the dataSource of UITableView, called for each cell and should work fast. So you must return reused cell instance as quickly as possible.
Don’t perform data binding at this point, because there’s no cell on
screen yet. For this you can use
tableView:willDisplayCell:forRowAtIndexPath: method which can be
implemented in the delegate of UITableView. The method called exactly
before showing cell in UITableView’s bounds.
From Perfect smooth scrolling
So I am trying to implement all my code in viewForHeaderInSection to willDisplayHeaderView (Note, I am using sections rather than rows for this specific example because I have custom sections). However, I am getting a "fatal error: unexpectedly found nil while unwrapping an Optional value" error at
let cell = tableView.headerViewForSection(section) as! TableSectionHeader
Below are my original and attempted code that crashed
Original (Note, this code works fine with some minor scrolling lagging problem that I am trying to improve)
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let cell = tableView.dequeueReusableHeaderFooterViewWithIdentifier("TableSectionHeader") as? TableSectionHeader {
// Cancel request of current cell if there is a request going on to prevent requesnt info from background from previous use of same cell
cell.AlamoFireRequest?.cancel()
var image: UIImage?
if let url = post.imageUrl {
image = DiscoverVC.imageCache.objectForKey(url) as? UIImage
}
cell.configureCell(post) // This is data binding part
cell.delegate = self
return cell
} else {
return TableSectionHeader()
}
}
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
}
Attempt
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let cell = tableView.dequeueReusableHeaderFooterViewWithIdentifier("TableSectionHeader") as? TableSectionHeader {
return cell
} else {
return TableSectionHeader()
}
}
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let cell = tableView.headerViewForSection(section) as! TableSectionHeader
// Cancel request of current cell if there is a request going on to prevent requesnt info from background from previous use of same cell
cell.AlamoFireRequest?.cancel()
var image: UIImage?
if let url = post.imageUrl {
image = DiscoverVC.imageCache.objectForKey(url) as? UIImage
}
cell.configureCell(post) // This is data binding part
cell.delegate = self
}
-----Update to address Michael's answer------
As there are word limits on replies, here is a response to the Answer from Michael
You are correct, I have updated where I got the snippet from in my questions. My mistake
I agree that problem could very well be lying else where, however this is something that I am going through at the moment. My tableview scrolls OK but sometimes when there is image it slows down a little. So I am going through some steps to ellimate any potential cause.
The reason that I specifically didnt use if let here is that becuase I was expecting the cell to be displayed will be TableSectionHeader. I tried to add it in just then and I ALWAYS gets a failed cast.
The reason that I subclass UITableViewHeaderFooterView is because my headerview is a Xib file where I have func configureCell() so I could call cell.configureCell. (And many other functions)
My header includes a few items like
labels to display title, date, time downloaded from firebase
image that can be optional
image description
like btn, commentbtn, more, btn
All of theses function are addressed in my TableSectionHeader.swift which inherits from UITableViewHeaderFooterView
Could you please explain what you mean by "It's looking suspiciously like you're trying to store state in the header - you should store state outside the tableView."?
Reason that I am cancelling Alamofire request here is because the cell gets dequeued. So if the user scrolls really fast, the cell would get many alamofire request. So I cancelled it first and re-open a download request (inside cell.configureCell) if I dont have anything in my cache
I am not sure how printing sections would help identify. I am thinking it is something foundamentally wrong that I am doing here putting everything in willDisplayHeaderView code (As most place you would put it in viewForHeaderInSection instead). Or maybe it is just the syntax
You're given the header view in the method, you just need to cast it. Try this:
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
guard let cell = view as? TableSectionHeader else { return }
cell.AlamoFireRequest?.cancel()
...
}
I don't think that comment comes from Apple, and I think your problem probably lies elsewhere (eg. blocking the main queue with a task that should run in the background). If you post the contents of TableSectionHeader, this may become clearer.
Pressing on regardless, you are getting a nil value exception in the following line:
let cell = tableView.headerViewForSection(section) as! TableSectionHeader
This is because you're forcing the cast as TableSectionHeader, and it undoubtedly isn't one. If you change it to an if let or guard let, you will see a different code path.
Since you obviously expect it to always be of that type (after all, that is what you're creating in viewForHeaderInSection), something is happening that you don't realise (why are you subclassing UITableViewHeaderFooterView anyway?). I'd be inclined to print the section number and view in both sections so you know what is really happening. It's looking suspiciously like you're trying to store state in the header - you should store state outside the tableView.
Also, there should be no need to cancel the Alamofire request, as this is not the cause of a performance problem - retrieving an image should just fire off another request. Otherwise while a user scrolls around, no images will be displayed because the requests will keep getting cancelled. They would have to leave the screen alone and wait for those images to load before scrolling elsewhere.
I have table contains custom cells contain uisegment
The problem is when select any item in uisegment and scroll down in the table view it change the selection in uisegment in the cells down on table
almost cell 1 like 11 and 2 like 12
It related to dequeueReusableCellWithIdentifier and my question is what is the best way to solve it ?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let item = self.cells.items[indexPath.row]
if let cellHedaer: TcPationQuestionHeader = tableView.dequeueReusableCellWithIdentifier("HeaderItem") as? TcPationQuestionHeader {
cellHedaer.setCell(Results.Questions[indexPath.row/2 ])
if item as? SwiftyAccordionCells.HeaderItem != nil {
let cellItem: TcPationQuestionItem = tableView.dequeueReusableCellWithIdentifier("Item") as! TcPationQuestionItem
cellItem.setCell(Results.Questions[indexPath.row/2])
return cellItem
}
cellHedaer.selectionStyle = UITableViewCellSelectionStyle.None
return cellHedaer
}
I see 2 major problems in your code:
You shouldn't keep the state inside UI. Every accessing data from UI
is a big mistake. You should keep selection inside model. The easiest way is to keep array var inside controller.
In some cases you will call dequeueReusableCellWithIdentifier 2 times. That shouldn't happen too.
Don't forget the implementation of cellForRow is connected to numberOfRowsAtIndexPath and numberOfSections. If you want to more detailed help paste here these 2 functions.
I'm trying to create an autocompleter using iOS 8, Swift and Xcode 6.3
I have a problem that I'm trying to solve, but I gave up... I hope someone can help here. The problem is that (custom) UITableViewCell's are not displaying when the initial dataSource is empty. When adding data to datasource and reloading the tableView, the cells SHOULD display, but they don't... At least, the first time they don't... A second time, they DO... When I initialize the table with non-empty data, the problem doesn't occur. I guess something goes wrong with dequeueReusableCellWithIdentifier. In beginning, no reusable cells are found, or something. But I don't know why...
Relevant code, in ViewController.swift:
// filteredWords is a [String] with zero or more items
#IBAction func editingChanged(sender: UITextField) {
autocompleteTableView.hidden = sender.text.isEmpty
filteredWords = dataManager.getFilteredWords(sender.text)
refreshUI()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! AutocompleteTableViewCell
cell.title.text = filteredWords[indexPath.row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredWords.count
}
func refreshUI() {
self.autocompleteTableView.reloadData()
}
I created a sample project on github:
https://github.com/dirkpostma/swift-autocomplete
And a movie on YoutTube to show what goes wrong:
https://www.youtube.com/watch?v=ByMsy4AaHYI
Can anyone look at it and spot the bug...?
Thanks in advance!
You've accidentally hidden your cell.
Open Main.storyboard
Select Cell
Uncheck Hidden
Side note: As for why it's displaying the second time around with the cell hidden? It appears to be a bug. It should still be hidden (print cell.hidden, notice it's always true despite showing the text on the screen).
I think you need to change your code. Check out below code. It is because if you remember in Objective C you needed to check if the Cell was nil and then initialise it. The reuse identifier is usually reusing an already created cell, but on the first launch this does not work because there is no Cell to use. Your current code assumes always that the cell is created (re-used) because you are using ! in the declaration, so if you use the optional (?) it can be null and you then can create the cell
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? AutocompleteTableViewCell
if cell == nil
{
//You should replace this with your initialisation of custom cell
cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "CELL")
}
cell.title.text = filteredWords[indexPath.row]
return cell