I have a UITableView with some sections, each has its own header view.
When user taps on the header view of a section, all rows of that section will collapse. What i do is, I set the number of row of that section to 0, and then call :
[self.tableView reloadSections:sections withRowAnimation:UITableViewRowAnimationBottom];
Everything works as expected, except one thing : the header view of the section becomes white blank. When i scroll the table, then the header becomes normal again.
So i guess there's some problem with the drawing of the table.
One funny thing is, if i use UITableViewRowAnimationFade instead, then even when i scroll the table, the header is still white blank.
When I update just ONE section there is also no problem - when I update more than one section the problem occurs.
If i use
[self.tableView reloadData]
instead, then everything works fine.
The reason i use
[self.tableView reloadSections:sections withRowAnimation:UITableViewRowAnimationBottom];
is because i want animation.
Wrapping with beginUpdates / endupdates does not work.
I realize that it's a LONG time since this question was posed but I think all of the answers given below are incorrect.
I had the same problem (with someone else's code) and was about to be fooled by this post when I realized that the code was not doing it's reuse of the table header correctly. The header disappearing was because the code was only supplying a UIView, not a UITableViewHeaderFooterView, registered correctly and set up for reuse.
Have a look at the answer here:
How to use UITableViewHeaderFooterView?
My blank headers went away when I set up a reusable header.
In Swift:
You need to create a class that's a subclass of UITableViewHeaderFooterView and register it to the table view. Then in viewForHeaderInSection, you do let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderView") as! YourHeaderView, similar to what you do for UITableViewCells. Then return that header.
The deceptive thing is the function calls for a return of UIView? when it really needs a dequeuedReusableHeaderFooterView or reloadData will cause it to disappear
I found a work-around - not very elegant, but it works.
Instead of providing a NSIndexSet with more than one section, I call the reloadSections within a for-loop with only one section in each call.
looks like:
for (Element *eleSection in self.gruppenKoepfe) {
if ( eleSection.type.integerValue == qmObjectTypeFormularSpalte || eleSection.type.integerValue == qmObjectTypeFormularZeile ) {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:nCount] withRowAnimation:UITableViewRowAnimationFade];
}
nCount ++;
}
I was having a similar problem, except I was trying to delete/insert sections, and I found that keeping a strong property pointing to the entire header view (i.e. not just a subview) stopped it disappearing during section updates.
e.g. Property and instantiation
#property (nonatomic, strong) UIView *headerView;
-(UIView *)headerView {
if ( !_headerView ) {
_headerView = [[UIView alloc] initWithFrame:CGRectMake(0,0,self.view.bounds.size.width, 300)];
// add additional view setup, including subviews
}
return _headerView;
}
And in tableView:(UITableView *)viewForHeaderInSection:(NSInteger)section:
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
if ( section == theCorrectSection ) {
return self.headerView;
}
return nil;
}
I had the same issue and it was none of the above. My problem was that i was hidding the first header and for some reason after more then 10 reloads the header at index 2 was set hidden. When i set headerView.hidden = false it was ok.
Related
I have a UITableView configured as plain style so I can have the header views stuck on the top of the table until another header pulls it away.
The problem is: If I have a header stuck on the top of the screen, and I programmatically scroll to another part of the table (where that header should not appear at all), that UIView will not be dismissed. I.e. if I scroll again to that part of the table, a ghost of that header will be visible on that part of the table.
I've implemented the method - (void)tableView:(UITableView *)tableView didEndDisplayingHeaderView:(nonnull UIView *)view forSection:(NSInteger)section to understand what is happening. I found that if I manually scroll until a header is pull away of the screen, this delegate is called. But if I scroll programmatically, the delegate is not called.
By the way, I tried scrolling programmatically using two different methods, and the problem is the same.
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;
- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;
One workaround that I can imagine is implementing - (void)scrollViewDidScroll:(UIScrollView *)scrollView;, filtering all the header views that are outside the visible screen, and removing them from superview. I can probably make it work, but I would like to know if there is any other better solution.
[EDIT] If I call - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; with animated = YES, the bug does not happen. I can go with this solution, but I really would like in some cases to scroll without animation.
Not entirely sure I understand your issue entirely but it seems that your header view(s) (some UIView) is/are not rendered correctly once you programmatically scroll away from this area / section and then return.
I'm not sure how you are filling your header view content but I have several applications running UITableView's with multiple section headers that require updating for scrolling / content offset's with no problem, as long as you "draw" your headers with this delegate:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
// Per section, simply return the appropriate header view
...
NSString *someIdentifier = [NSString stringWithFormat:#"sectionHeaderView:<some #, letter, or tag>", <SOMETHING UNIQUE ADD HERE>];
UITableViewHeaderFooterView *myHeaderView = [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:someIdentifier];
if (!myHeaderView) {
// No header view found with that ID. Make a new one
}
...
return myHeaderViewForSection;
}
This way whether you finger scroll or programmatically set the content offset which ever way you like, your table view will know what to draw, when to draw it, and where to put it.
Using their delegates is a bit of a drag as it's slightly tedious at start, but using the viewForHeaderInSection proved the only way I ever obtained the results I (you) wanted.
Hope this helps - happy coding!
TL;DR
Do NOT explicitly scroll the table view between beginUpdates and endUpdates.
Explanation
I'm using NSFetchedResultsController to populate the table. These are my implementations for some of the methods of NSFetchedResultsControllerDelegate.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[_conversationTableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[_conversationTableView endUpdates];
}
The problem is that endUpdates was making a chain of calls that ended calling my method [self scrollToBottom] (which was a very ugly code actually). This method, as the name says, calls - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; to scroll the table view to the bottom of the table.
The explicit scrolling of the table during a beginUpdates - endUpdates was the culprit of my whole problem.
Solution
Scrolling the table view only after finishing endUpdates.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[_conversationTableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[_conversationTableView endUpdates];
[self scrollToBottom];
}
Side Note
This also fixed a problem where the table view was sometimes flickering when scrolling.
Manually set the sectionHeader height to 0 when it should not appear
tableView.sectionHeaderHeight = 0;
I've got a project (that has been written by other people) where there's a feed with content and text displayed inside a table view. Each post corresponds to one section in table view, and each section has its own rows corresponding to elements like content, text, like button etc.
I need to display a short label for post captions with a "more" button inside table view cell, and when more button is tapped, the label will expand to whatever size the caption fits, all happening inside a table view cell. When the more button is tapped I change the label's numberOfLines property to zero, and as the cells have automatic height, all I need is to reload that particular caption cell. (the cell displays correctly with the expanded size if I set numberOfLines to 0 at the first place before displaying the cell.)
I've tried:
[tableView beginUpdates];
tableView endUpdates];
I've tried various animation options with:
[tableView reloadRowsAtIndexPaths:#[myPath] withRowAnimation:UITableViewRowAnimation(Bottom,Top,None etc)];
I've tried:
[tableView reloadSections:[NSIndexSet indexSetWithIndex:myPath.section] withRowAnimation:UITableViewRowAnimation(Top,Bottom,None etc)];
But they all yield the same result: the whole table view layout gets messed up: it jumps to another cell, some views go blank, cells overlap each other, video inside cells stop playing, and the label doesn't expand (but refreshes inside itself, e.g. that short preview txt with one line animates from top/bottom etc but doesn't expand).
What might be causing the mess up of the whole table view and how I can reload just one cell correctly with and expansion animation, without messing up the whole layout? I've seen many questions and answers regarding this, but they all recommend the options that I've already tried and explained above.
My app targets iOS 8.0+
UPDATE: Here is the relevant code (with some parts regarding inner workings that aren't related to layout, removed):
MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier: MyCellIdentifier forIndexPath:indexPath];
cell.delegate = self;
cell.indexPathToReloadOnAnimation = indexPath;
cell.shouldShortenCaption = YES;
id post = self.posts[indexPath.section] ;
[cell setPost:post];
return cell;
And inside setPost::
if(self.shouldShortenCaption){
captionLabel.numberOfLines = 2;
}else{
captionLabel.numberOfLines = 0;
}
NSString *text = [some calculated (deterministic) text from post object];
Button action is simple:
self.shouldShortenCaption = NO;
[one of the reload codes that I've written above in the question]
UPDATE 2: Here are some more methods regarding the issue:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (section < self.posts.count) {
return 59;
}else
return 0;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
if (section < self.posts.count) {
MyFeedHeader *header = [tableView dequeueReusableCellWithIdentifier:MyFeedHeaderIdentifier];
header.delegate = self;
[header setPostIndex:section];
[header setPost:self.posts[section]] ;
return header;
}
else
return nil;
}
Header's setPost: method basically sets the relevant texts to labels (which have nothing to do with the caption, they are completely different cells. the problematic cell is not the header cell). The table doesn't have any footer methods. The only method regarding height is the one above.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section >= self.posts.count) {
return 1;
}
id post = self.posts[section];
[calculate number of rows, which is deterministic]
return [number of rows];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
BOOL hasCursor = self.cursor && self.hasMore ? 1 : 0;
return self.posts.count + hasCursor;
}
(Post count and cursor and hasMore are also deterministic).
UPDATE 3: I've asked a question (which wasn't a duplicate, even though there are similar questions) and got a useful answer that solved my problem. Can the downvoters please elaborate the reason that they've downvoted?
Here is an example: https://github.com/DonMag/DynamicCellHeight
Table B is one way of accomplishing "More/Less" (Table A was for another layout I played around with). It uses the [tableView beginUpdates]; tableView endUpdates]; method of triggering the table re-layout.
The key is getting all your constraints set up correctly, so the Auto-Layout engine does what you expect.
The example is in Swift, but should be really easily translated back to Obj-C (I think I did it in Obj-C first).
Edit: some additional notes...
This is using a pretty standard method of dynamic-height table view cells. The vertical spacing constraints between elements effectively "pushes out" the bounds of the cell. The tap here toggles the numberOfLines property of the label between 2 and 0 (zero meaning as many lines as necessary). Sequential calls to beginUpdates / endUpdates on the table view tells the auto-layout engine to recalculate the row heights without needing to reload the data.
For this example, I did use a little "trickery" to get the smooth expand/collapse effect... The multiline label you see here is contained in a UIView (with clipsToBounds true). There is a second, duplicate multiline label (alpha 0 so it's not visible) that is controlling the height. I found that changing the numberOfLines on the visible label sort of "snapped" to 2 lines, and then the size change animation took place... resulting in the text "jumping around."
Just for the heck of it, I added the "not so good" version to my GitHub repo for comparison's sake.
I know this question has been asked before. But no person on the internet had a working and sufficient answer.
EDIT Obviously people don't read questions anymore, on SO. So I'm trying to clarify: I want to remove the SEPARATOR. The separator is neither the space above the section, nor the tableViewHeader or tableViewFooterView. It is only the thin line above (fully from left to right).
I have a grouped UITableView (I don't want to use a plain styled for many other reasons, take it as it is) which has multiple groups.
The first section should not have the separator line on top. Setting the separator style of the tableView is not an option, because I do need the other separators.
Setting the tableViews tableFooterView is something I often read, but it never worked.
I used the tableView with static content before and I was able to remove the separator in -[UITableViewController viewDidLoad] using this:
- (void)viewDidLoad {
[[[self headerTableCell] valueForKey:#"_topSeparatorView"] removeFromSuperView];
}
Since I now had to change the tableView to a dynamic one, the IBOutlet property won't work anymore (obviously).
So I tried everything, -[id tableView:willDisplayCell:atIndexPath:], -[UITableViewCell initWithStyle:reuseIdentifier:, prepareForReuse, awakeFromNib] and some others.
In any case, this separator is nil. So I need a method that gets called when the complete view hierarchy of the cell is setup.
what i get from your situation you have a grouped UITableView you want the first section without separator and you want to keep the separator in the other sections so
remove the separator from the whole tableview from the attributes inspector make Separator : None
create custom UITableviewCell in storyboard for other sections and add View at the end of it with height 1 and width the whole screen (like default separator)
it's maybe not the best idea but this will allow you to have the first section without separator
I faced a similar problem, wanted to remove the last line of the section in grouped table view, I am calling following method in view will appear and on every table reload. This is not the exact answer but problem can be solved by just changing y value of dummy view.
+(void)removeLastSectionSeparatorForTableView:(UITableView *)tableView
{
UIView *oldSeparatorView = [tableView viewWithTag:kTagDummySectionSeparator];
if (oldSeparatorView != nil)
{
[oldSeparatorView removeFromSuperview];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(),
^{
UIView *view = [[UIView alloc] init];
view.tag = kTagDummySectionSeparator;
view.backgroundColor = [UIColor colorWithRed:239.0/255 green:239.0/255 blue:244.0/255 alpha:1.0];//Group table background color
view.frame = CGRectMake(0,
tableView.contentSize.height-40,
tableView.bounds.size.width,
2);
[tableView addSubview:view];
});
}
Maybe this problem is the same as mine before.
Finally, my way to solve this problem: set table view delegate's method (CGFloat)tableView:(UITableView *)tableView heightForHeaderAtSection:(NSInteger)section, then return CGFLOAT_MIN;
add this override function in your Custom Cell Class
override func layoutSubviews() {
super.layoutSubviews()
for subview in subviews where (subview != contentView && abs(subview.frame.width - frame.width) <= 0.1 && subview.frame.height < 2) {
subview.removeFromSuperview()
}
}
I'm using a UITableView with static cells for the settings view of my application.
One of the sections of this table contains two cells, the second one only being visible when a UISwitch contained in the first one is turned on.
So the UISwitch's target is this:
- (void)notificationSwitchChanged:(id)sender
{
UISwitch* switch = (UISwitch*)sender;
self.bNotify = switch.on;
[self.settingsTable reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationTop];
}
I implemented numberOfRowsInSection: this way:
(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
if (section != 1)
return [super tableView:tableView numberOfRowsInSection:section];
if (self.bNotify)
return 2;
return 1;
}
This "works", but the animation is faulty, making the first cell disappear or slide upwards halfway below the previous section or various stuff depending on the animation type I choose. The cleanest is UITableViewRowAnimationNone but is still not perfect.
If I scroll the section out of view and back it looks normal again.
EDIT: took a few screenshots of the problem:
I tried adding beginUpdates and endUpdates before and after reloadSections but it didn't change anything. Using reloadData instead of reloadSections works but there's no animation at all.
Is it because the table's cells are static or am I missing something?
I think this method should solve your problem.
[self.tableView insertSections:<#(NSIndexSet *)#> withRowAnimation:<#(UITableViewRowAnimation)#>];
Just update the data source (you dont have to because your are updating the section count depending on some flags) and call it.
Maybe you have to use this method instead.
[self.tableView insertRowsAtIndexPaths:<#(NSArray *)#> withRowAnimation:<#(UITableViewRowAnimation)#>];
I'm trying to change the header title for a section in a UITableView when a cell from that section is selected. tableView:titleForHeaderInSection is triggered by the application, so that doesn't help. I can call reloadData, but the performance suffers because the app has to reload all the visible cells. I also tried to use a custom header, but that also results in some performance problems.
Is there any way to get a handle to the UILabel that the default header view uses and change its text manually?
Calling [tableView endUpdates] may provide the desired results without too much of a performance hit.
[self.tableView beginUpdates];
[self.tableView endUpdates];
// forces the tableView to ask its delegate/datasource the following:
// numberOfSectionsInTableView:
// tableView:titleForHeaderInSection:
// tableView:titleForFooterInSection:
// tableView:viewForHeaderInSection:
// tableView:viewForFooterInSection:
// tableView:heightForHeaderInSection:
// tableView:heightForFooterInSection:
// tableView:numberOfRowsInSection:
with:
[self.tableView headerViewForSection:i]
you could get the view for Section i and then "update" it manually
this even works if your view is just the auto-generated label, but you will have to resize it yourself.
so if you try to:
[self.tableView headerViewForSection:i].textLabel.text = [self tableView:self.tableView titleForHeaderInSection:i];
you will set the text, but you would not set the label-size. You can get the needed size from NSString to set it yourself:
[label.text sizeWithFont:label.font];
There doesn't appear to be any standard API for accessing the system-provided section header view. Have you tried the more targeted reloadSections:withRowAnimation to get UIKit to display the new header text?
What kind of performance issues were you seeing with custom section header views? I doubt that the standard one is much more than just a UILabel.
You can set the title for the section header label directly. For example, to set the title of section zero:
UITableViewHeaderFooterView *sectionZeroHeader = [self.tableView headerViewForSection:0];
NSString *sectionZeroLabel = #"Section Zero";
[sectionZeroHeader.textLabel setText:[sectionZeroLabel uppercaseString]];
[sectionZeroHeader setNeedsLayout];
Be sure and tell the section header view it needs layout, otherwise the new text may be truncated. Also, section labels are usually all uppercase.
Since a UITableView does not enqueue and dequeue section header views for reuse, you might as well see if it's feasible to store all the section header views in memory. Note you'll have to create your own section header views with the background and etc., to do this, but it allows you a bit more flexibility and capability.
You could also try tagging the section header views (also requires you to create your own section header views) and just grab them from the tableview as needed.
This is a WAG, and I can think of lots of reasons why it might not work, but couldn't you iterate through the subviews, and find the one you are looking for? E.g.
for (UIView *v in self.tableView.subviews) {
// ... is this the one?
}
One of the solution would be to manage external array for multiple section headers containing label references and update them externally.
For completeness, the method we are all looking for is this private API named exactly what you'd expect:
-(void)_reloadSectionHeaderFooters:withRowAnimation:
eg:
[tableView _reloadSectionHeaderFooters:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationAutomatic]
I do not recommend using this, however.
If you want to update only one section best way is tableView.headerView. If header is not visible it returns nil, so it doesn't load extra headers.
if let header = tableView.headerView(forSection: i) {
header.textLabel!.text = "new title"
header.setNeedLayout()
}
If you want to update all visible section headers it is better to set header tag as section before view is displayed and enumerate subviews when needed:
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
view.tag = section
// additional customization
}
func udpateVisibleSectionHeaders() {
for subview in tableView.subviews {
if let header = subview as? UITableViewHeaderFooterView {
let section = header.tag
header.textLabel!.text = "new title"
header.setNeedsLayout()
}
}
}
Don't forget to call setNeedsLayout or label can be truncated.