Refresh only the custom header views in a UITableView? - ios

My UITableView has custom UIView headers in every section. I am needing to refresh only the headers and not the other content in the section. I have tried out [self.tableView headerViewForSection:i] and it does not return anything even though it should. Is there anyway that I can do this?
Edit: Code based around new suggestion
I have given this a shot as well and it calls/updates the UIView within that method, but the changes do not visually propagate onto the screen.
for (int i = 0; i < self.objects.count; i++) {
UIView *headerView = [self tableView:self.tableView viewForHeaderInSection:i];
[headerView setNeedsDisplay];
}

Instead of calling setNeedsDisplay, configure the header yourself by setting it's properties. And of course you have to get the actual headers in the table, don't call the delegate method, because that method usually creates a new header view.
I usually do this in a little helper method that is called from tableView:viewForHeaderInSection: as well.
e.g.:
- (void)configureHeader:(UITableViewHeaderFooterView *)header forSection:(NSInteger)section {
// configure your header
header.textLabel.text = ...
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UITableViewHeaderFooterView *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:#"Header"];
[self configureHeader:header forSection:section];
}
- (void)reloadHeaders {
for (NSInteger i = 0; i < [self numberOfSectionsInTableView:self.tableView]; i++) {
UITableViewHeaderFooterView *header = [self.tableView headerViewForSection:i];
[self configureHeader:header forSection:i];
}
}

Create your own subclass of UIView for the headerView, and add a couple of methods to it so your view controller can send whatever UI updates you want to it.

Calling setNeedsDisplay only serves to trigger the drawRect: method of the receiving view. Unless this is where your text color logic is (that is, in the drawRect method of your UIView subclass that you are using for header views), nothing is likely to change. What you need to do is directly call something on the header views to update their state based on your new or changed data or directly access the label in the header view and change it yourself while iterating through them. setNeedsDisplay will only trigger code found in the drawRect: method of the view.

I recently had this exact problem and only overcame it by keeping a pointer to the headers as they were created. I did it in Swift and with a swift only dictionary type but the idea could be massaged into objC somehow.
var headers : [ Int : UIView ] = [ : ] //dictionary for keeping pointer to headers
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let h = super.tableView(tableView , viewForHeaderInSection: section) { //or create your view (this viewController inherits it)..
self.headers.updateValue(h , forKey: section) //keep a pointer to it before you return it..
return h
}
return nil
}

When you create your custom header views, use UITableViewHeaderFooterView instead of a simple UIView and when you initialize it, use something like this:
UITableViewHeaderFooterView *cell = [[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:#"reuseIdentifier"];
The reuseIdentifier will tell your UITableView to keep track of the header and it should no longer return null when you try to access it with [self.tableView headerViewForSection:i]
You should then be able to use setNeedsDisplay on the UITableViewHeaderFooterView you get back from headerViewForSection after making your modifications.

Swift 5 Solution:
Delegate method of your table view:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// Ideally, dequeue your header view...
var view: UITableViewHeaderFooterView?
if section == 0 { view = ... // dequeue appropriately
} else if section == 1 { view = ... // dequeue appropriately
} else ... // etc.
setupHeaderView(&view)
return view
}
In the area of code where you want to do the header view update:
var headerView = tableView.headerView(forSection: i)
setupHeaderView(&headerView)
Header setup method:
func setupHeaderView(_ view: inout UITableViewHeaderFooterView?) {
// do something with `view` here
}

tableView.reloadSections([0,0], with: .automatic)
https://developer.apple.com/documentation/uikit/uitableview/1614954-reloadsections

Swift 5 :
If you are using custom view as a table header view then try below code
-- Make your custom class as a subclass of UITableViewHeaderFooterView.
class MyCustomHeaderView : UITableViewHeaderFooterView {
#IBOutlet weak var sampleLabel: UILabel!
}
-- Then fetch your view using below method (using section number)
func updateHeaderView() {
// This is for section no. 1, you can use `for loop` for all sections
if let headerView = self.myTableView.headerView(forSection: 1) as? MyCustomHeaderView {
// Set values or update constraints to your view
headerView.sampleLabel.text = "your new text goes here"
headerView.sizeToFit() //Resizes and moves the receiver view so it just encloses its subviews
self.myTableView.beginUpdates()
self.myTableView.endUpdates()
// if you are using for loop then keep this `beginUpdates` and `endUpdates` outside loop
}
}

Related

Dynamically change the section header text on a static cell

I have a UITableViewController, with its table view having static cells, defined in a storyboard.
My table view has two sections. The first with two cells, and the second section has three cells. The second section also has text in its header.
What I would like to do is that when the user taps the first or second cells in the first section, to update the header text of the second section. Do so dynamically and with dynamic content (say the date and time is displayed there as of the moment they tap cells).
I have tried numerous things, but the viewForHeaderSection is only called once.
I registered the header section cell with
tableView.registerClass(TableSectionHeader.self, forHeaderFooterViewReuseIdentifier: "secondSectionCell")
Where TableSectionHeader is simply:
class TableSectionHeader: UITableViewHeaderFooterView { }
I am then able to dynamically set the section header, like so:
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 1 {
if let cell = tableView.dequeueReusableHeaderFooterViewWithIdentifier("secondSectionCell") {
cell.textLabel?.text = "hello world"
return cell
}
}
return nil
}
I also have implemented the following override, since some people suggest that when implementing viewForHeaderInSection, it is also required:
override func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 40.0
}
Even still. viewForHeaderInSection is only called once.
Am I able to somehow refresh the section header text dynamically as described above?
You can actually achieve this using traditional table view way easily.
Even though it is static UITableView, in your dataSource view controller, you can still implement - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
So how do you update the title on the fly? Create a property for this view controller, say NSString *titleForSecondSection. Whenever user tap the cells in the first section, you just need to update this property in the callback - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
The last step is to call [self.tableView reload] after you modified the titleForSecondSection property. Or if you don't want to reload the whole table view, just call - (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
To be clear, in your - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section, for sections that don't need to change title, just return a static string. For sections that need to change title, return the dynamic property you created.
viewForHeaderInSection would only be called when the tableview is reloaded. So assuming you don't want to reload the whole tableview, you might need to change the content of label directly.
pseudo codes like:
var label_first_section_header //as global variable
then in viewForHeaderInSection just point it to the label
if section == 1 {
if let cell = tableView.dequeueReusableHeaderFooterViewWithIdentifier("secondSectionCell") {
cell.textLabel?.text = "hello world"
label_first_section_header = cell.textLabel
return cell
}
}
then you can change the text dynamically whenever you want, for example in didSelectRowAtIndexPath

How do I prevent UIaccessibility from saying "heading" for controls created inside viewforheaderinsection?

So, I have a tableview and inside the viewforheaderinsection, I create a view, create some controls such as buttons and segmented controls programmatically. I add those controls as subview of the view and then return the view. The problem is when Accessibility reads the controls, it appends "heading" at the end. It says "button" pauses and then says "heading". I know I can convert the headerview to cells to suppress the "heading" callout but that is not an option. The project is pretty big and it requires a lot of time to change headerviews to cells. Is there a way to suppress the "heading" callout without changing headerview to cell?
You need to implement
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section;
for example
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
view.accessibilityTraits = UIAccessibilityTraitNone;
}
inside that function, set the accessibility trait for the header to UIAccessibilityTraitNone, or simply remove UIAccessibilityTraitHeader.
Doing it inside
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
will not do any good because the iOS will add the header trait later on. You need to modify the trait right before the header is being displayed.
The answer by #cocoaaficionado did not work for me on iOS 10.3.
What I had to do was the following:
For your header view, you need to froce by subclassing, the accessibilityElements property of the view, and use UIAccessibilityElements instead of UIView/UILabel/UIButton directly.
#interface NotAHeadingView : UIView
#end
#implementation NotAHeadingView
- (void)layoutSubviews
{
[super layoutSubviews];
NSMutableArray *accessibilityElements = [NSMutableArray arrayWithCapacity:self.subviews.count];
for (UIView *view in self.subviews) {
UIAccessibilityElement *element = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
// remove Header trait
element.accessibilityTraits = view.accessibilityTraits & ~UIAccessibilityTraitHeader;
element.accessibilityFrame = view.accessibilityFrame;
element.accessibilityLabel = view.accessibilityLabel;
element.accessibilityValue = view.accessibilityValue;
element.accessibilityHint = view.accessibilityHint;
element.accessibilityFrameInContainerSpace = view.frame;
[accessibilityElements addObject:element];
}
self.accessibilityElements = accessibilityElements;
}
#end
UIViews seem to automatically inherit the Header trait from the view hierarchy, even overriding traits for their direct superview isn't enough. This worked on an iOS 10.3 device for me.
This works for simple cases, but did not work for me when applied to a very complex section header view (multiple child viewControllers managing the subviews, deep view hierarchy including scrollViews) - instead it was much easier to resign how this view worked to avoid section headers for these elements. YMMV
Swift solution
Subclassing header elements and overriding their accessibilityTraits property was the easiest solution for me.
For example, I had the same situation for buttons in header, and here's the solution that works for me:
class HeaderButton: UIButton {
private var _accessibilityTraits: UIAccessibilityTraits = UIAccessibilityTraits.button
override var accessibilityTraits: UIAccessibilityTraits {
get {
return _accessibilityTraits
}
set {
_accessibilityTraits = newValue
_accessibilityTraits.remove(UIAccessibilityTraits.header)
}
}
}
#abalas solution works for me:
Try using.
private var _accessibilityTraits: UIAccessibilityTraits = UIAccessibilityTraits.none
override var accessibilityTraits: UIAccessibilityTraits {
get {
return _accessibilityTraits
}
set {
_accessibilityTraits = newValue
_accessibilityTraits.remove(UIAccessibilityTraits.header)
}
}
Rewrite setAccessibilityTraits of these controls such as buttons and segmented controls.
For example:
- (void)setAccessibilityTraits:(UIAccessibilityTraits)accessibilityTraits
{
accessibilityTraits = UIAccessibilityTraitButton;
[super setAccessibilityTraits:accessibilityTraits];
}

UITableView duplicate section header after updating row heights

In my iOS app, I have a UITextView inside a tableview cell.
The UITextView and hence the cell height expands when the frame required for the text entered by user exceeds the current height of the cell.
In order to achieve the above, I am calling [tableView beginUpdates] followed by [tableView endUpdates] to reload the height for the cells.
The above is resulting duplicate section headers overlapping the expanded cell.
Is there a way to fix this without calling [tableView reloadData]?
Appended below is some relevant code:
When there is a text change, I verify if the text will fit in current text view, if not the cell is expanded to the new height:
- (void)textViewDidChange:(UITextView *)textView {
CGFloat oldTextViewHeight = [(NSNumber *)[self.cachedTextViewHeightsDictionary objectForKey:indexPath] floatValue];
CGFloat newTextViewHeight = [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)].height + CELL_HEIGHT_PADDING;
if (newTextViewHeight > oldTextViewHeight ||
(newTextViewHeight != oldTextViewHeight && oldTextViewHeight != TEXTVIEW_CELL_TEXTVIEW_HEIGHT)) {
[self reloadRowHeights];
}
}
- (void)reloadRowHeights {
// This will cause an animated update of the height of the UITableViewCell
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
It's also important to note that I am using a custom section header, which makes my problem similar to one mentioned here:
UITableView Custom Section Header, duplicate issue
I cannot however use the solution to above problem because I cannot reloadData for the tableView in middle of user entering text.
Try implementing
tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int)
delegate method if you didn't
Little late to the party, but I couldn't find a working solution on SO, and then I figured one out, so I thought I'd share.
I use UITableViewAutomaticDimension both for cell heights and for section header heights. My header view class is just a UIView subclass with some subviews as needed. Inside my tableView(:viewForHeaderInSection:) class, I just initialized a new header view as needed, and I was experiencing this duplicate headers issue. Not even reloadData helped.
What seems to have fixed it for me was to implement basic "cell re-use" for the headers. Something like this:
Store the header views in a dictionary somewhere in your view controller.
class ViewController: UIViewController {
var sectionHeaders: [Int: UIView] = [:]
// etc...
}
Then, upon request, return your existing section header view if available, or else create and store a new one.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let sectionHeader = self.sectionHeaders[section] {
return sectionHeader
} else {
let sectionHeader = YourSectionHeader()
// Setup as needed...
self.sectionHeaders[section] = sectionHeader
return sectionHeader
}
}

Swipe to delete cell causes tableViewHeader to move with cell

I have encountered a strange bug with my tableViewHeader on my UITableView in iOS 8. When swiping on a cell to reveal the delete button (standard iOS swipe-to-delete), it moves the tableViewHeader along with the cell that is being swiped. As I swipe the cell, the header moves in the same way that the cell being swiped does. No other cells in the table view are moved, only the header and whatever cell is being swiped. I have tested this on iOS 7 haven't encountered the problem. To me, this seems like a bug with tableViewHeader in iOS 8, being that it only occurs in this version and seems like something that should never occur. I see no reason for the header to ever be included in swipe-to-delete.
Below is just a mockup. Swipe-to-delete within the app is default iOS, nothing custom.
Building on ferris's answer, I found the easiest way when using a UITableViewCell as a section header is to return the contentView of the cell in viewForHeaderInSection. The code is as follows:
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell : cellSectionHeader = tableView.dequeueReusableCellWithIdentifier("SectionHeader") as cellSectionHeader
return cell.contentView
//cellSectionHeader is my subclassed UITableViewCell
}
This was caused because I was using a UITableViewCell as the header for the table. To solve the swiping issue, instead of using tableView.tableHeaderView = cell, I use the following:
UIView *cellView = [[UIView alloc] init];
[cellView addSubview:cell];
tableView.tableHeaderView = cellView
I don't know why this solves the problem, especially being that it worked on iOS 7, but it seems to solve the problem.
Make sure to add all view to the cells view, as supposed to the cells contentView, otherwise the buttons will not be responsive.
Works:
[cell addSubview:view]; or [self addSubview:view];
Doesn't work:
[cell.contentView addSubview:view] or [self.contentView addSubview:view]
The way to avoid the headers moving with the cells is to return the contentView of the cell in viewForHeaderInSection. If you have a subclassed UITableViewCell named SectionHeaderTableViewCell, this is the correct code:
-(UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
SectionHeaderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"SectionHeader"];
//Do stuff to configure your cell
return cell.contentView;
}
SWIFT 3.0 Tested solution. As mentioned in the first example for Objective-C; the key point is returning cell.contentView instead of cell So new format syntax is as below.
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// HeaderCell is the name of custom row designed in Storyboard->tableview->cell prototype
let cell = tableView.dequeueReusableCell(withIdentifier: "HeaderCell")
cell.songLabel.text = "Your Section Name"
return cell.contentView
}
Try implementing this method and give proper conditions for checking the swipe.If this method get called for header view.
1.tableView:editingStyleForRowAtIndexPath: 2.tableView:titleForDeleteConfirmationButtonForRowAtIndexPath: 3.tableView:shouldIndentWhileEditingRowAtIndexPath:
One problem not mentioned with the .contentView method is that if your table view cell makes use of layoutSubviews, you may not get the behavior you want-- because layoutSubviews will not get called. I ended up making an entirely separate UIView subclass that supports both normal cell operation and header operation, and creating a bare minimum UITableView cell class that uses that.
Victor's issue of losing background color, is solved by below addition to Brad's answer:
cell.backgroundColor = UIColor.cyanColor()
To:
cell.contentView.backgroundColor = UIColor.cyanColor()

UITableViewHeaderFooterView with IB

After many years of avoiding Interface Builder like the plague I decided to give it a chance. It's not easy.
Take UITableViewHeaderFooterView for example. Like UITableViewCell, it has a contentView property. Unlike UITableViewCell, it doesn't have a template in the Interface Builder object library.
How are we supposed to use Interface Builder to create a UITableViewHeaderFooterView with the content inside contentView? The fact that registerNib:forHeaderFooterViewReuseIdentifier: exists makes me think this should be possible somehow.
This is the closest I got to define a UITableViewHeaderFooterView with IB:
a. Create a UITableViewHeaderFooterView subclass (MYTableViewHeaderFooterView).
b. Create a nib file for the contentView only (MYTableViewHeaderFooterContentView).
c. Override initWithReuseIdentifier: in MYTableViewHeaderFooterView to load the view defined in the nib file.
- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithReuseIdentifier:reuseIdentifier];
if (self)
{
NSArray* objects = [[NSBundle mainBundle] loadNibNamed:#"MYTableViewHeaderFooterView"
owner:self
options:nil];
UIView *nibView = [objects firstObject];
UIView *contentView = self.contentView;
CGSize contentViewSize = contentView.frame.size;
nibView.frame = CGRectMake(0, 0, contentViewSize.width, contentViewSize.height);
[contentView addSubview:nibView];
}
return self;
}
d. Register the MYTableViewHeaderFooterView class instead of the nib file:
[self.tableView registerClass:[MYTableViewHeaderFooterView class] forHeaderFooterViewReuseIdentifier:#"cell"];
I just did this with a footer and a NIB file:
Create an empty NIB file with name CustomFooterView.xib.
Edit the NIB file in the Interface Builder and change the topmost UIView custom class to UITableViewHeaderFooterView.
Disable Auto Layout in the NIB.
Set the background color of UITableViewHeaderFooterView view to Default.
Make the view freeform and correct size (for example 320 x 44).
In your UITableViewController viewDidLoad register the NIB file to be used with a reuse identifier:
[self.tableView registerNib:[UINib nibWithNibName:#"CustomFooterView" bundle:nil] forHeaderFooterViewReuseIdentifier:#"Footer"];
In your UITableViewController's tableView:viewForFooterInSection: use the Footer identifier to fetch and return the view:
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
if (section == 2)
return [tableView dequeueReusableHeaderFooterViewWithIdentifier:#"Footer"];
return nil;
}
Just use the UITableViewCell template in IB. Change the class to UITableViewHeaderFooterView. Here you have it... with a contentView.
I found easier way.
1) Create subclass of UITableViewCell and set your xib file
2) In your header file change superclass from UITableViewCell to UITableViewHeaderFooterView
That's it.
This solution works well, especially if you want it to work correctly in relation to Readable Content Guides (introduced in iOS 9). Instead of creating a UITableViewHeaderFooterView, it simply returns a custom UIView (from a XIB) when it is required:
Create a new class that subclasses UIView ("AwesomeHeaderView") and create your outlets:
class AwesomeHeaderView: UIView {
#IBOutlet var myCustomLabel: UILabel!
}
Create a XIB file ("MyNewHeader.xib") with a UIView as
the parent view. Change the parent UIView's class type to your newly created custom class ("AwesomeHeaderView"). As required, add any additional views as it's children and link outlets etc. (NB: To ensure views comply to the new Readable Content Guides I check the boxes "Preserve Superview Margins" and "Follow Readable Width" on all objects).
In your UIViewController (or
UITableViewController) call the following:
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let headerView = NSBundle.mainBundle().loadNibNamed("MyNewHeader", owner: nil, options: nil).first as? AwesomeHeaderView else {
return nil
}
// configure header as normal
headerView.backgroundColor = UIColor.redColor()
headerView.myCustomLabel.textColor = UIColor.whiteColor()
headerView.myCustomLabel.text = "Hello"
return header
}
I also experienced the deprecation warning above and an inability to set background color, etc.. Eventually, I found that according to Apple's documentation,
You can use [the UITableViewHeaderFooter] class as-is without subclassing in most cases. If you have custom content to display, create the subviews for your content and add them to the view in the contentView property.
Following that suggestion, I took these steps:
I created a xib with a view that extends only UIView--not UITableViewHeaderFooter.
In viewDidLoad I registered the built-in UITableViewHeaderFooter class
tableView.registerClass(UITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: "sectionHeader")
In the viewForHeaderInSection delegate of my UITableViewController, I dequeued the header view by that identifier and checked to see if the header view already contained a subview. If not, I load my xib and add it. Then I set my text as needed.
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier("sectionHeader")!
if header.contentView.subviews.count == 0 { header.contentView.addSubview(loadMyNib()) }
let myView = header.contentView.subviews[0] as! MyView
myView.label.text = "..."
This seems to work, leverages reuse, and does not produce any warnings.
https://developer.apple.com/library/prerelease/tvos/documentation/UIKit/Reference/UITableViewHeaderFooterView_class/index.html#//apple_ref/occ/instm/UITableViewHeaderFooterView/prepareForReuse
Following workarounds enable me to drag-assign IB items to code as variables. UITableViewHeaderFooterView doesnt allow that out of the box.
create a (New File/CocoaTouchClass) UITableViewHeaderFooterView
.h.m.xib normally
temporarily rename superclass from UITableViewHeaderFooterView to UIView. Drag-assign your UI items to code as needed, IB will assign key-value
correctly, revert back to UITableViewHeaderFooterView when done.
in your tableview, use registerNib: to register instead of registerClass:. prepare the rest of tableview normally (ie:dequeue).
An awful hack I figured out is to create a IBOutlet contentView ih your headerFooter class and hook it up to the your "content view" in the xib (Have your xib laid out like a tableViewCell, View->contentView->mystuff).
You'll get warnings, ok ready for the hack...
Delete the IBOutlet and it will all work.
The best way I found:
Create your HeaderFooterView(.h/.m), such as header
Create a xib(view), name the same
Make the root view class as your class(header)
[important] Connect all outlets to the root view, not the File's Owner
Last, register your nib to table(All done).

Resources