UITableView duplicate section header after updating row heights - ios

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
}
}

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

UITableView inside UITableViewCell with dynamic cell height

I have a dynamic list of items, each item could have different teamplate/layout (and height). And one those item types could have an internal list of items to select from, regularly 5-6 rows, each has different height.
If I try to describe it further, in my scenario I have one tableview (#slave) inside tableviewcells (#master-cell) of another tableview (#master). Moreover cells (#slave-cell) in my #slave tableview could have different height as well. So I need to layout my #slave to have #master automatically calc and update its size.
I have the issue with the inner table (#slave). In case of auto-layout, to fit all the cell space, the table will be collapsed unlike UILabel or other controls. So what I need here is to get the projected size of #slave table and set the height of the #slave = content height of the #slave.
I found similar post and it works if all rows have the same height but I'm using custom rows with dynamic height so the tableView.contentSize.Height gives me invalid result (basically just multiply rowNumbers * estimatedRowHeight, you can see it on the screenshot, master item #3 has 4 inner cells). Even after calling #slave.reloadData I couldn't get that size.
What is the proper way to build that kind of UI?
Source code with a test project attached (Xamarin.iOS)
I just ran into the same problem a few days ago,and tried to work it around.
The #master-cell works like a childViewController,it's the delegate datasource of the #slave TableViewController.But you cann't have a childViewController in the UITableViewcell.
Customize UITableViewCell to hold necessary property and acts as #slave TableViewController's delegate datasource,and configure #slave-cell's
height and data.
The real problem is the height for #master-cell,
If your data is simple and static,you can compute the height in advance,and return it in method func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat of the ViewController.
Otherwise,add a method to #master-cell which return the height for the whole cell when its property is set.And create a proxy #master-cell to compute the height and return it :
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell = CustomUITableViewCell();
let model = self.getModel(indexPath)
cell.model = model
let height = cell.requiredHeight()
return height;
}
It's complex and expensive,but it works.
I think you do not have need of take UITableView inside UITableView. You can take more than one section in UITableView. And use different cellReuseIdentifier. This way your goal will be achieved.
For such a layout ios provide section in tableview, for master items use SectionView(there is delegate method for sectionView -> in which you can provide view for a section) and as different section may have different type of row so make rows according your need and return them according to section.
Perhaps it is because I do not know the background of you project or what you are trying to accomplish, but tableViews inside of tableVIew cells sounds unnecessarily trivial. Rather than using a master tableView with #slave tableViews, it would be cleaner to just break things out by section in a single tableView as stated in a previous answer. There are UITableViewDelegate methods designed to streamline this for you!
first you have to get string's height then the height have to give in below tableView delegate
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return stringHeight;
}
it is working for me.
I'm using Xcode 8.3.2 and Swift3.1.
I had the same requirement, have tried all, nothing worked for me.
Finally, UIStackView is what worked for me.
In a tableviewcell, I have added a UIStackView(Verticle), keep adding sub cells to that UIStackView. And it automatically increased the cell height.
Check the following to add UIStackView programmatically.
Add views in UIStackView programmatically
If you Use Different Sections and Rows use the below format, its working for me,
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
return 121;
}
if(indexPath.section==1)
{
return 81;
}
if (indexPath.section%2 == 0 && indexPath.row == 1) {
return 161;
}
if (indexPath.section%2 != 0 && indexPath.row == 0) {
return 81;
}
if (indexPath.section==16 && indexPath.row==0) {
return 161;
}
else
{
return 44;
}
}
i have Template code, different section and row, its each row have different sizes, so i have give this type of code, if you get idea see the above code then its helpful for you,
or
If you change the height for Content text size use the below code, its calculate the content size then change the height(UILabel) size, its working for me
-(CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
ListModel *model = [ListArray objectAtIndex:indexPath.row];
CGRect labelRect = [model.content boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 90 - 15, 0)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{
NSFontAttributeName : [UIFont fontWithName:#"Arial" size:14.0]
}
context:nil];
CGFloat heightOfCell = labelRect.size.height + 60;
if(heightOfCell > 106)
return heightOfCell;
return 106;
}
hope its helpful
yes of course you can have as many prototypes cells as you want for example check this piece of code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier("TodayWeatherCell", forIndexPath: indexPath) as! SITodayWeatherTableViewCell
cell.setupCell(upCommingWeather)
cell.aboutCityUpdateTableViewClousure = {
self.tableView.reloadData()
}
return cell
}else {
let cell = tableView.dequeueReusableCellWithIdentifier("cityDetailCell", forIndexPath: indexPath) as! SICityDetailTableViewCell
let detail = detailCity[indexPath.row]
cell.setupCityDetail(detail)
return cell
// Configure the cell...
}
}
There are two different cells in one single UITableView.
Hope it helps.

iOS 8 UITableView first row has wrong height

I'm working on an app where I face a strange issue. I've created a UITableViewController in the storyboard and added a prototype cell. In this cell, I've added an UILabel element and this UILabel takes up the whole cell. I've set it up with Auto Layout and added left, right, top and bottom constraints. The UILabel contains some text.
Now in my code, I initialize the the rowHeight and estimatedRowHeight of the table view:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 50
}
And I create the cell as follows:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("HelpCell") as? UITableViewCell
if(cell == nil) {
cell = UITableViewCell(style: .Default, reuseIdentifier: "HelpCell")
}
return cell!
}
I return two rows in my table view. Here comes my problem: the height of the first row is way to large. It appear that the second, third row etc all have a correct height. I really don't understand why this is the case. Can someone help me with this?
I had a problem where the cells' height were not correct on the first load, but after scrolling up-and-down the cells' height were fixed.
I tried all of the different 'fixes' for this problem and then eventually found that calling these functions after initially calling self.tableView.reloadData.
self.tableView.reloadData()
// Bug in 8.0+ where need to call the following three methods in order to get the tableView to correctly size the tableViewCells on the initial load.
self.tableView.setNeedsLayout()
self.tableView.layoutIfNeeded()
self.tableView.reloadData()
Only do these extra layout calls after the initial load.
I found this very helpful information here: https://github.com/smileyborg/TableViewCellWithAutoLayoutiOS8/issues/10
Update:
Sometimes you might have to also completely configure your cell in heightForRowAtIndexPath and then return the calculated cell height. Check out this link for a good example of that, http://www.raywenderlich.com/73602/dynamic-table-view-cell-height-auto-layout , specifically the part on heightForRowAtIndexPath.
Update 2: I've also found it VERY beneficial to override estimatedHeightForRowAtIndexPath and supply somewhat accurate row height estimates. This is very helpful if you have a UITableView with cells that can be all kinds of different heights.
Here's a contrived sample implementation of estimatedHeightForRowAtIndexPath:
public override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! MyCell
switch cell.type {
case .Small:
return kSmallHeight
case .Medium:
return kMediumHeight
case .Large:
return kLargeHeight
default:
break
}
return UITableViewAutomaticDimension
}
Update 3: UITableViewAutomaticDimension has been fixed for iOS 9 (woo-hoo!). So you're cells should automatically size themselves without having to calculate the cells height manually.
As the Apple says in the description of setNeedsLayout:
This method does not force an immediate update, but instead waits for the next update cycle, you can use it to invalidate the layout of multiple views before any of those views are updated. This behavior allows you to consolidate all of your layout updates to one update cycle, which is usually better for performance.
Because of this you should add needed lines of code (which should be executed with right layout) in dispatch_after block(which will put your method in queue of RunLoop). And your code will be executed after needs layout applies.
Example:
- (void)someMethod {
[self.tableView reloadData];
[self.tableView setNeedsLayout];
[self.tableView layoutIfNeeded];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//code which should be executed with the right size of table
});
In iOS 8, assigning the estimatedRowHeight a value turns on the new iOS 8 automatic row height calculation feature. This means that the cell's height is derived by using its internal constraints from the inside out. If there's something wrong with those constraints you'll get odd results. So there's something wrong with your cell constraints. They are probably ambiguous; that is the usual reason for inconsistency. However that's all I can tell you, since you have not actually shown / described the constraints.
I suggest removing the bottom constraint on the UILabel. It will resize according to the text and the cell should resize as well.
If that didn't resolve the issue, try adding the following in viewDidLoad() :
self.tableView.reloadData()
This worked for me
- (void)layoutSubviews
{
[super layoutSubviews];
// Your implementation
}
I got this from here >> http://askstop.com/questions/2572338/uitableview-displays-separator-at-wrong-position-in-ios8-for-some-cells

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()

Refresh only the custom header views in a UITableView?

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
}
}

Resources