How do I read a UITableViewCell (subclass)'s height from a NIB? - ios

I have a UITableViewCell (really a subclass) defined in a NIB. In the NIB I've set the frame height to 183, I also set the "Row Height" to 183 and ticked off custom.
In my original question stuff all went wrong here. It looked like I was getting a wrong height back, and the cell loaded at the wrong height and later resized to the right height making a nasty visual glitch.
In reality I was doing some stupid stuff (and always returning the wrong height), and it was too late at night for me to figure it out, so I asked SO for help and went to sleep. The sleep (+coffee) made everything clear.
So for the record, if you load your custom UITableViewCell from a NIB you can look at frame.size.height and the right number comes back.
If you return the wrong number from tableView:heightForRowAtIndexPath: you can get this glitch, at least if the errant cell is the last one in the table (I expect the problem will manifest in another way for other cells)
So I've solved my own problem, and hopefully left enough info for anyone else who hits this. If anyone thinks this ought to have an official answer rest assured I plan on accepting the best looking one in a day or 3.

You can use this example code to get cell height from NIB
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [[[[NSBundle mainBundle] loadNibNamed:#"ELConvCurListViewCell" owner:self options:nil] objectAtIndex:0] bounds].size.height;
}
Update: You can get and assign height of NIB once time in the viewDidLoad to the cellHeight variable and in the heightForRowAtIndexPath use return cellHeight

First UITableViewDelegate method, which gets called is heightForRowAtIndexPath not the UITableViewDataSource method, cellForRowAtIndexPath.
You can get your custom cell height only in cellForRowAtIndexPath, where you alloc-init that cell. So, it won't consider custom cell height over here.
It will take the height for the cell which is mentioned in heightForRowAtIndexPath method only. As it gets called first.

Related

UITableView with dynamic cell heights jumping when scrolling up after reloading cell

I have a table view with the potential for each cell to have its own height, thus isn't suitable to use rowHeight. Instead, right now I'm using let indexSet = NSIndexSet(index: 10), and self.tableView.estimatedRowHeight = 75. This means that it calls the sizeThatFits function on the cell, to determine its' height. This all works well.
The problem is then when you reload a cell that's onscreen. Scrolling down to show cell 10, for example, then reloading cell 10, works fine. But when you begin to scroll back up, past the cells you've already seen, it reverts to the estimatedRowHeight for each cell, completely disregarding sizeThatFits, and so jumps around as you scroll. It'd be impossible for me to give an accurate or 'good enough' estimatedRowHeight so that this jumping wouldn't be noticeable, as my cells will be able to display either a line of text, or a full image - a big difference in size.
I've shown this effect here:
https://vid.me/edgW
I've made many different attempts at this, using a mixture of heightForRowAtIndexPath, estimatedHeightForRowAtIndexPath.. etc.. I've tried various pieces of advice on StackOverflow. Nothing seems to work.
I've attached a very simple sample project, where you can try it for yourself:
https://www.dropbox.com/s/8f1rvkx9k23q6c1/tableviewtest.zip?dl=0
Run the project.
Scroll until cell 10 is in view.
Wait up to 5 seconds for the cell to reload (it becomes purple).
Scroll up.
Worth noting - this does not happen if the cell is not in view when it's reloaded. If it's either above or below the current scroll point, everything works as expected.
This behavior appears to be a bug, if for no other reason than it's no longer reproducible on iOS 9. I'm sure that's not much consolation.
The issue primarily derives from having an inaccurate estimate, like #NickCatib said. The best you can do on iOS 8 is to improve the estimation. A technique many have recommended is to cache heights in willDisplayCell and use them on subsequent calls to estimatedRowHeightAtIndexPath.
You might be able to mitigate the behavior by not doing anything to get UITableView to discard its caches, like by modifying the content in a cell directly using cellForRowAtIndexPath rather than using reloading if it's onscreen. However, that won't help if you actually need to change the height of the cell.
I'm afraid to say the bug can't be easily be fixed within a table view, as you don't have control over the layout. The bug can be more easily worked around in a subclass UICollectionViewFlowLayout by changing the contentOffsetAdjustment during invalidation, although that might not be terribly easy.
I had this issue too and used a solution from SO which I am unable to find right now. If I do, I will add the link. Here's the solution:
The issue is with table view not having the right estimate for height of rows. To fix it, cache the height initially in willDisplayCell and next time use that height.
Code
In viewDidLoad:
heightAtIndexPath = [NSMutableDictionary new];
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *height = #(cell.frame.size.height);
[heightAtIndexPath setObject:height forKey:indexPath];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
if([heightAtIndexPath objectForKey:indexPath]) {
return [[heightAtIndexPath objectForKey:indexPath] floatValue];
} else {
return UITableViewAutomaticDimension;
}
}
In iOS 13.5.1:
I have a tableView which contains 4 types of cells , all off them having different and dynamic heights.
I have followed the accepted solution here, but to solve jumping effect I need to add more when reloading table view:
From accepted answer I have added below code:
Declare this variable
var allCellHeights = [IndexPath : CGFloat]()
Then add 2 methods:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
self.allCellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.allCellHeights[indexPath] ?? UITableView.automaticDimension
}
Now the extra code I have to add when reloading tableview:
let contentOffset = self.tableView.contentOffset
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
self.tableView.setContentOffset(contentOffset, animated: false)
also check this answer : reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling
Uh... this is kind of hard issue to deal with.
Let's take a look at facebook. They had this same issue with their timeline, and they ended up doing it in some kind of web view.
I had a similar issue with some kind of timeline, used automatic row height and had that issue. First thing to resolve it was to set estimatedHeight as close as possible to average cell height. That is pretty hard to deal with since you may have text (height 50) or images + text ( height 1500). Next thing to do with improving this was implementing estimatedHeight forIndexPath which basicly return different estimated height for different indexPaths.
After that there were a lot of other solutions but this was as closest as it can with variable heights (huge differences).
I was facing the same problem, my table works fine until the tableview reloads. So i found a solution, use only rowHeight not estimated height. Also if you have different height. So please provide the complete code so i will provide a solution. I have a cell which like instagram page. I am passing calculated height in heightforrow method which work fine. But estimated height not works fine in that situation. If you use below code, it works fine. Please try.
self.tableView.rowHeight = 75 //it will be your dynamic height
//self.tableView.estimatedRowHeight = 75
If you confuse to calculate the height for row for each cell, just post the sample i will provide the solution. If i can
Thanks

How to set height of row depend on 80 percentage height of main View/Display

I have a view controller and a tableview inside of that. I want to set height of the row 80% of main view, Is there any method without using autolayout? Please help me to find the proper solution.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return self.view.frame.height * 0.8
}
substituted self.view for your mainView
ObjC if you need it:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return (self.view.frame.size.height * 0.8);
}
The only problem with this answer, as Michael above has sort of indicated, there's absolutely nothing wrong with constraints and you really should be doing something like this with constraints. There's no guarantee that my solution is going to work for you because I can't garantee when the system is going to trigger a call to the cell height vs the mainView, and if the mainView's height isn't triggered FIRST, the you have a big problem on your hand because now your cells are height ZERO. It would be best, if you need a custom height for a cell, to subclass a cell, use autolayout like a boss, and do this the RIGHT way, but feel free to implement this method, it will work, but you now know when it won't work. Good luck
And should it not work, just say something, there's always people more than willing to help you through this, it's no big deal!

How to ask a UITableViewCell its height?

Following https://stackoverflow.com/a/7313410/1971013 I need to get the height of each of my cells in - (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath.
But when I do:
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath
{
MyTableViewCell* cell = (MyTableViewCell*)[self.tableView cellForRowAtIndexPath:indexPath];
return cell.height;
}
this (of course) results in unwanted recursion blowing up my app.
Any ideas of how I should do this?
The best answer is, sadly, I don't know. That's up to you here. The heightForRowAtIndexPath from UITableViewDelegate is what the UITableView uses to ask you what the height should be. In this function you are determining that.
Perhaps you can save it in your own memory in cellForRowAtIndexPath after you dequeueReusableCellWithIdentifier.
But if your linked questions are any indication, you're trying to expand a selected cell, correct? You can determine if it's the selected cell via
if ([indexPath compare:[tableView indexPathForSelectedRow]] == NSOrderedSame)
return 88; // some expanded value
else
return 44; // the default value
Gutblender is right: in the usual case, you don't ask a cell its height, you tell it.
It's important to embrace the fact that cells are recycled. From a practical perspective, you can't examine cells that are off screen because they don't exist.
Cautions aside, here are the options:
Create a method that defines the height of your table view cells, and use that method to calculate the value you return from heightForRowAtIndexPath. You can then call that method from elsewhere to determine what the height of the cell will be at a given index path, whether or not it exists. This is the standard approach. It's also, 99.9% of the time, the correct one.
Iterate through the table view's subviews. A bad idea because you don't control the NSTableView class, and the view hierarchy can change. And, again: cells are recycled.
Create custom table view cell class, use something like the MVVM design pattern and let your model objects vend the table view cells. You won't have to worry about the view hierarchy, but again: cells are recycled – you will end up managing stale or incorrect state.
One method I've used on static table view's that don't have any reuse and the cells are pre-generated, is create a subclassed tableView cell with a height property, so in heightForRowAtIndexPath you can get the cell for that indexPath and return the cell's height you hard-coded in that you wanted for the cell. That way, any time you want to change the height you can change it directly on the cell itself and when you reloadData it should update the cell height.

setNeedsLayout relayouts the cell only after is has been displayed

I have a table view with cells, which sometimes have an optional UI element, and sometimes it has to be removed.
Depending on the element, label is resized.
When cell is initialised, it is narrower than it will be later on. When I set data into the label, this code is called from cellForRowAtIndexPath:
if (someFlag) {
// This causes layout to be invalidated
[flagIcon removeFromSuperview];
[cell setNeedsLayout];
}
After that, cell is returned to the table view, and it is displayed. However, the text label at that point has adjusted its width, but not height. Height gets adjusted after a second or so, and the jerk is clearly visible when all cells are already displayed.
Important note, this is only during initial creation of the first few cells. Once they are reused, all is fine, as optional view is removed and label is already sized correctly form previous usages.
Why isn't cell re-layouted fully after setNeedsLayout but before it has been displayed? Shouldn't UIKit check invalid layouts before display?
If I do
if (someFlag) {
[flagIcon removeFromSuperview];
[cell layoutIfNeeded];
}
all gets adjusted at once, but it seems like an incorrect way to write code, I feel I am missing something else.
Some more code on how cell is created:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ProfileCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
[cell setData:model.items[indexPath.row] forMyself:YES];
return cell;
}
// And in ProfileCell:
- (void)setData:(Entity *)data forMyself:(BOOL)forMe
{
self.entity = data;
[self.problematicLabel setText:data.attributedBody];
// Set data in other subviews as well
if (forMe) {
// This causes layouts to be invalidated, and problematicLabel should resize
[self.reportButton removeFromSuperview];
[self layoutIfNeeded];
}
}
Also, if it matters, in storyboard cell looks like this, with optional constraint taking over once flag icon is removed:
I agree that calling layoutIfNeeded seems wrong, even though it works in your case. But I doubt that you're missing something. Although I haven't done any research on the manner, in my experience using Auto Layout in table cells that undergo a dynamic layout is a bit buggy. That is, I see herky jerky layouts when removing or adding subviews to cells at runtime.
If you're looking for an alternative strategy (using Auto Layout), you could subclass UITableViewCell and override layoutSubviews. The custom table cell could expose a flag in its public API that could be set in the implementation of tableView:cellForRowAtIndexPath:. The cell's layoutSubviews method would use the flag to determine whether or not it should include the optional UI element. I make no guarantees that this will eliminate the problem however.
A second strategy is to design two separate cell types and swap between the two in tableView:cellForRowAtIndexPath: as necessary.
You've added additional code to the question, so I have another suggestion. In the cell's setData:forMyself: method, try calling setNeedsUpdateConstraints instead of layoutIfNeeded.

UICollectionView's cell disappearing

What's happening
Currently I have an application that uses two UICollectionViews inside a UITableView. This way I create a Pulse News look like application. My problem with this is that sometimes the 6th and 11th row disappears completely, leaving a blank space where it should be the cell. I wouldn't actually mind, if all the cells were like this (and this way I could assume that I wasn't doing things correctly), but the thing is, is just happening with those specific ones.
My theory
The 6th and 11th rows are the ones that appears when I start scrolling, so by default I am able to see 5 cells, and when I do the first horizontal scrolling the 6th comes up (blank space sometimes).
What I have
The only thing I am doing at the moment is this:
[self.collectionView registerNib:[UINib nibWithNibName:CELL_NIB_NAME bundle:nil] forCellWithReuseIdentifier:CELL_IDENTIFIER];
On the viewDidLoad. And on the creation of the cell:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CELL_IDENTIFIER forIndexPath:indexPath];
NSMutableDictionary *dictionary = [self.DataSource objectAtIndex:[indexPath row]];
[cell buildViewWithDictionary:dictionary withReferenceParent:self.referenceViewController];
return cell;
}
So on my understating nothing fancy going on here. I though there was something wrong on the data source (a dummy JSON file), but sometimes it works ok and the cell shows, so I guess from that part is ok.
So my "question": Does anyone knows what's going on? I don't really like to say that it's a bug from iOS, but I can't think of anything else.
Edit 1.0
The amazing part is that this method
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
Is going from indexPath [0,4] to [0,6] without calculating the [0,5]. First time I actually see this happening in iOS.
Edit 2.0
I have switched the way I am creating the cells, and instead of dequeuing I am using the old way:
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:CELL_NIB_NAME owner:self options:nil];
MyCell *cell = (MyCell *)[nib objectAtIndex:0];
Still the same sad result.
So, what did work?
1) Subclass UICollectionViewFlowLayout.
2) Set the flowLayout of my UICollectionView to my new subclass.
3) On the init method of the UICollectionViewFlowLayout subclass, set the orientation you want:
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
In my case it is Horizontal.
4) The important part:
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
At this moment, I should theorise a bit, but honestly I don't have a clue.
The above answers didn't work for me, but after downloading the images, I replaced the code
[self.myCollectionView reloadData]
with
[self.myCollectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
to refresh the collectionview and it shows all cells, you can try it.
None of the solutions given by anyone helped me in my custom layout that we need to have in our app.
Instead, I had to do this: (And yeah, IT WORKS!!!)
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
CGSize size = [self collectionViewContentSize];
rect.size.height = size.height*2;
NSArray *atrributes_Super = [super layoutAttributesForElementsInRect:rect];
return atrributes_Super;
}
After all, UICollectionView is just looking for the attributes for the elements to be displayed in your screen's rect.
Rob's tip about the bug helped me. The bug states that if the section insets and cells widths and spacing add up exactly to the width of the screen then the first column sometimes randomly dissappears and reappears for some cells in some places. Rather than subclass or change the cell widths, I changed the section insets for left and right in my storyboard from 6 to 4 and it I haven't seen the problem again.
As I run the same problem suddenly and spent some time figuring out one of possible reasons of cell disappearing during the scroll, I will add my answer as well.
Prerequisites of the problem:
You have a UICollectionView instance
You have a UICollectionViewFlowLayoutSubclass
The problem
Cells disappear from the Collection View after scrolling to the certain point.
The source of the problem is in wrong subclassing of the UICollectionViewFlowLayout.
As it explicitly said in documentation:
Every layout object should implement the following methods:
- collectionViewContentSize
- layoutAttributesForElements(in:)
- layoutAttributesForItem(at:)
- layoutAttributesForSupplementaryView(ofKind:at:) // (if your layout supports -supplementary views)
-layoutAttributesForDecorationView(ofKind:at:) // (if your layout supports decoration views)
- shouldInvalidateLayout(forBoundsChange:)
By relying on UICollectionViewFlowLayout implementation of methods above we miss the fact, that func layoutAttributesForElements(in rect: CGRect) and collectionViewContentSize will generate wrong contentSize (the size that would be correct if all the cells would have itemSize size and the content size would be corresponding. As soon as scroll offsetY will be greater that contentSize height cell will all disappear.
The solution
The solution is in proper UICollectionViewFlowLayout subclassing. Override all the methods that are required to override and everything will work just fine.
In my case (vertical scroll, with cells disappearing in first view), cells were disappearing due to incorrect estimated size. It seems, UICollectionView uses the estimated size to calculate the items to load in first view. I'd set the estimated size too high which was resulting in wrong calculations for number of items to load in first screen.
The moment I made the estimated height bit low, all the cells appeared correctly.
We ran into disappearing cells recently and found that rather than skipping 'hidden' cells we were accidentally inserting 0x0 sized cells. The resulting behavior was very confusing and did not suggest these invisible cells were the issue. We would see the correctly sized cells and layout, but a few of the valid cells would consistently disappear after scrolling off/on screen. I have no idea why intermingling 0 sized cells would cause this behavior, but removing them fixed the problem. This may not be causing your problem, but this may be helpful to devs searching for similar symptoms.
Just ran into an issue where all UICollectionView cells were disappearing on scroll.
This happened because I had declared
extension UICollectionViewLayout {
static let defaultLayout: UICollectionViewLayout {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
return layout
}()
}
... meaning the same layout instance was being used in multiple UICollectionViews. I had meant to make that a computed var. Hope this helps someone who's accidentally using the same layout object in multiple collection views.
What caused the cells to disappear in my case was that the data source was being deallocated prematurely. UICollectionView.dataSource is a weak property, which means that unless you keep a strong reference to it, the object will be deallocated at the end of the scope in which you created. The problem manifested itself with disappearing cells as soon as I tapped on the UICollectionView.
For me this issue seemed to be related with the way i make my collectionview adapt to an open keyboard to prevent content overlaps.
in my observer to respond to KeyboardWillShow i had this:
var userInfo = obj.UserInfo[UIKeyboard.FrameEndUserInfoKey];
if (userInfo is NSValue value)
{
var rect = value.CGRectValue;
var windowOffset = this.Superview.ConvertPointToView(this.Frame.Location, UIApplication.SharedApplication.KeyWindow);
var newHeight = rect.Y - windowOffset.Y;
this._controller.CollectionView.Frame = new CGRect(0, 0, this._controller.CollectionView.Frame.Width, newHeight);
}
After changing it to this:
var userInfo = obj.UserInfo[UIKeyboard.FrameBeginUserInfoKey];
if (userInfo is NSValue value)
{
var rect = value.CGRectValue;
UIEdgeInsets contentInsets = new UIEdgeInsets(0, 0, rect.Height, 0);
this._controller.CollectionView.ContentInset = contentInsets;
this._controller.CollectionView.ScrollIndicatorInsets = contentInsets;
}
The cell disappearance issue completely went away. This is C# from working with xamarin but i hope it helps someone else.
I think this is not a UICollectionView‘s bug, maybe your not return right data in - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect method.
You can see this demo: https://github.com/lqcjdx/YLTagsChooser , all cells can appear when scolling the UICollectionView.

Resources