I have an expandable tableview with custom headers. I need to change a text of a label in the header when the table is expanding. I used below code for this.
func toggleSection(header: SuperHeaderDelegate, section: Int) {
sections[section].expanded = !sections[section].expanded
tableView.beginUpdates()
tableView.reloadSections([section], with: .automatic)
tableView.endUpdates()
if let head = header as? SavingAccountHeaderView {
head.accNoLabel.text = "HIIIIIII"
}
else { print("NOPE") }
}
When I use this code it changes the accNoLabel text to new text and change it back to the old text again.
I have tried the tableView.reloadData() instead of the tableView.reloadSections(), then the code worked fine. The accNoLabel didn't change back to its old text. But I really need to use the tableView.reloadSections() to use the animation.
So could anyone tell me what I'm doing wrong?
Couple of things you should check:
Is your 'expanded' toggle happening at the right place (and time)?
reloadSections() is an asynchronous function. So you can't expect your text to be updated 'after' the updates have been completed.
I would suggest toggling your label's text inside the viewForHeader function, because at this point you can be sure that the reloadSections() method is being called and the section's state has already been updated.
Related
I have a UIViewController that has a UITableView which presents comments fetched form a live Firebase database.
Every time a new comment arrives, I call
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: self.liveComments.count-1, section: 0)], with: .fade)
tableView.endUpdates()
to insert the latest comment with a fade animation. This works fine.
However, each cell has a label that shows when it was posted, in the form of "seconds, minutes or hours ago". The problem is that when many comments arrive, the age label does not get updated, since the existing cells are not updated, and it looks to the user like the comment ages are wrong.
I've tried calling
tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows ?? [], with: .none)
inside my tableView updated block, but the animation is all messed up, since all of the visible cells seem to get animated in a weird, "jumpy" way.
I've also tried getting all of the visible cells, and calling a method on them to update their timestamp labels manually, but I get a crash when I do this, so I guess it's not recommended:
if let visibleCells = self.tableView.visibleCells as? [LiveCommentTableViewCell] {
visibleCells.forEach { cell in
cell.updateCommentAgeLabel()
}
How can I approach this? I just need to reload all visible cells without an animation, and the last cell with a fade in animation. Thank you!
I would just reload all the data, as long the cellForRowAt sets the timestamp label correctly it should work fine:
// still do your nice animation
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: self.liveComments.count-1, section: 0)], with: .fade)
tableView.endUpdates()
// now just refresh the entire table
tableView.reloadData()
of course you're going to want to make sure that whatever collection feeds the numberOfItemsInSection is also updated before calling reloadData() im assuming you're already doing this as well or you'd be running into a lot of bugs and crashes
make sure that code that edits UI is on the main thread too, obviously.
That being said what does your cell.updateCommentAgeLabel() function look like bc that would work in theory as well unless potentially its not being called on the main thread again or the cast isn't working.
Perhaps try telling the system you want it to do a layout pass:
if let visibleCells = self.tableView.visibleCells as? [LiveCommentTableViewCell] {
visibleCells.forEach { cell in
cell.updateCommentAgeLabel()
cell.layoutIfNeeded() // either this
}
tableView.layoutIfNeeded() // OR this at the end, I dont expect you'll need to do both but not sure if both work
I'm using a table view to display a checklist. Some views group list items by type. For these I display the type name with the item count in parentheses (e.g. "Things to do today (58)").
I want to refresh that string to reflect the updated count when the user deletes or completes an item.
This can be accomplished easily by setting the header's textLabel property and calling setNeedsLayout(). Problem is, it changes the appearance of the text.
Before (all caps default section title font, e.g. "THINGS TO DO TODAY (58)"):
screenshot - before
After (no longer all caps, e.g. "Things to do today (58)"):
screenshot - after
Here's the code I'm using currently to refresh the header:
func refreshHeaderTitleForSection(_ section: Int) {
if let header = tableView.headerView(forSection: section) {
header.textLabel?.text = getHeaderTitleForSection(section)
header.setNeedsLayout()
} else {
return
}
}
I'm calling getHeaderTitleForSection() to get the string for titleForHeaderInSection when the table view loads, so it should be the same. Is there a way to tell it to keep the default formatting without defining a custom header view?
Presumably, your getHeaderTitleForSection method returns the non-uppercase string. UITableView automatically shows section headers in all uppercase regardless of what you return from titleForHeaderInSection.
The easiest solution is for you to force all uppercase when you update your header view.
header.textLabel?.text = getHeaderTitleForSection(section).uppercased()
This is a problem stumping me and my team at work.
We have a header view we are using in one section that is not always drawing. It's inconsistent, though appears to be slightly more frequent on fresh installs.
I will first preempt by stating we are not registering it to the table view and thus not actually dequeueing it. I tried that but the boss says since we are never actually re-using it he is adamant against doing so. Therefore we have it like this:
class DashboardViewController {
...
var trendsHeader: TrendsHeader?
...
override func viewDidLoad() {
super.viewDidLoad()
setupTrendsHeader()
....
}
...
func setupTrendsHeader() {
trendsHeader = Bundle(for: TrendsHeader.self).loadNibNamed(TrendsHeader.identifier, owner: view, options: nil)?.first as? TrendsHeader
trendsHeader?.delegate = self
trendsHeader?.datasource = self
trendsHeader?.leftDropdown.selectRowWithAction(at: 0)
}
...
// in heightForHeaderInSection
return TrendsHeader.cellHeight //Height is returned properly, empty space is of the right height
...
// in viewForHeaderInSection
return trendsHeader ?? UIView()//Should only return UIView if trendsHeader is nil
}
I have tried modifying viewForHeaderInSection as such:
//in viewForHeaderInSection
if trendsHeader == nil {
setupTrendsHeader()//Breakpoint inserted here
}
return = trendsHeader ?? UIView()
And with a breakpoint in the if statement so I should know if the trendsHeader is nil. It doesn't hit the breakpoint but still doesn't draw that header. If I wait anywhere from 5–30 seconds the header will show up, or if I scroll down the header will show up once it redraws the section. But I need it to show up initially as well, which it still does in most runs of the app but sometimes just doesn't. Honestly the most frustrating part is the inconsistency.
Any insight as to why this is occurring and/or a resolution that doesn't involve convincing my boss to register a view for re-use that isn't going to be re-used?
We found it. Long story short, the section in question is tied to 2 service calls, one for each option on the header's drop down menu. Those services had delegates who reloaded the section upon the callback functions, and they were getting them in quick succession causing the glitch.
We replaced the reload section code with reload rows code instead, so the header does not get refreshed.
In a tableview’s section. I have some selectable rows.
Inside the section’s header I have a Deselect All button.
The deselect all button needs to get enabled if a row is selected and disabled if no row is selected.
The button is enabled/disabled by reading a boolean value. I have a didSet on that boolean:
var shouldEnableDeselectButton = false
{
didSet{
if oldValue != shouldEnableDeselectButton{
DispatchQueue.main.async {
self.notesTable.reloadSections([self.indexofFilter], with: .none)
}
}
}
}
If I do absolutely nothing in the didSet then it only gets updated by being dequeued/scrolling up or down and getting off the screen.
See below:
If I do self.notesTable.reloadSections([someSection], with: .none) then it longer requires the section header to get dequeued. Yet if I select one of the last rows, then it jitters badly:
See below:
I looked online and found solutions mentioned Reload tableview section without scroll or animation but they both still jitter
2 things were needed:
Get a reference/pointer to the headerView. E.g. this answer.
Have your button as a property. I was originally having it just as a subview. That wasn't enough!
To do such I made my sectionHeader a UITableViewHeaderFooterView subclass and then added the button as a property.
Then I was simply able to change the isEnabled property of the button.
I have a collection view with three different cells. Each of the cells contains a table view. So, there are three table views. I've set tags for each of them (from 1 to 3).
Now, on my view controller (I set it as the table view's data source when I dequeue collection view's cells) I call table view's data source method for the number of rows. To distinguish table views I check each one's tag. Here is the code for that:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch tableView.tag {
case 1:
if unitItems1 != nil {
return unitItems1!.count
} else {
return 0
}
case 2:
if unitItems2 != nil {
return unitItems2!.count
} else {
return 4
}
case 3:
if unitItems3 != nil {
return unitItems3!.count
} else {
return 4
}
default:
return 0
}
}
The problem is, when the first cell of the collection view is shown (with the first table view) it works fine. But when I scroll to the second cell, BOTH case 2 and case 3 get executed. After that, the second table view shows data as expected, but when I scroll to the third one, the method doesn't get called.
I can't figure out why two case statements get called one after another, while everything works fine for the first cell. If you have any ideas why this happens (or maybe, you could suggested a better way of checking table view's), I would appreciate your help.
Actually, the solution is quite simple. The reason of the problem was that collectionView's data source method was dequeueing all the cells one after another, even when they weren't on the screen. Consequently, tableView's inside of each cell were getting set, too. So, their data source method was getting called, hence the problem.
UICollectionView has a property called isPrefetchingEnabled. According to the documentation it denotes whether cells and data prefetching is enabled.
The documentation says:
When true, the collection view requests cells in advance of when they will be displayed, spreading the rendering over multiple layout passes. When false, the cells are requested as they are needed for display, often with multiple cells being requested in the same render loop. Setting this property to false also disables data prefetching. The default value of this property is true.
So, to solve the problem, described in the question, I set it to false as soon as my collectionView gets set.