iOS UITableView AutoSizing Cell with Section Index - ios

I have an app where I load a large plist file in a tableview. This plist file is as a book. It contains chapters and lines. Each row has different length depending on the line length and therefore I need to resize the cell automatically.
I am using storyboard and standard tableview and cell. Cell style=basic and the cell label is set to text=plain lines=0 linebreaks=wordwrap
Up to there, no problem resizing the cell height to the proper size. As the cell height is defined before the text is inserted in the label we have to do it by the well known method of using CGsize and I do it like that (it's working fine)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:
(NSIndexPath*)indexPath
{
NSUInteger section = [indexPath section];
NSString *key = [chapters objectAtIndex:section];
NSArray *linesSection = [lines objectForKey:key];
NSString* theText = [linesSection objectAtIndex:indexPath.row];
int labelWidth = LW_CHAPTERINDEX_10;
if (chapters.count < 100){
if (chapters.count < NB_MIN_CHAP){
labelWidth = LABELWIDTH;
} else {
labelWidth = LW_CHAPTERINDEX_10;
}
}
CGSize textSize = [theText sizeWithFont:[UIFont systemFontOfSize:14]
constrainedToSize:CGSizeMake(labelWidth, MAXFLOAT)
lineBreakMode:UILineBreakModeWordWrap];
return textSize.height;
}
The problem is the hardcoding depending on the index. For now I have 3 possibilites.
No index
Index with numbers below 10
Index with numbers below 100
and in the code this part
int labelWidth = LW_CHAPTERINDEX_10;
if (chapters.count < 100){
if (chapters.count < NB_MIN_CHAP){
labelWidth = LABELWIDTH;
} else {
labelWidth = LW_CHAPTERINDEX_10;
}
}
is the hardcoding depending on the 3 possibilities.
I find this was of doing weird, especially if apple will start to deliver more different screen sizes.
QUESTION
How can I get the index width at runtime to determine my label width ?
For example i would like to program something like screen.width - index-width to get the label width.
Or any other that should allow it to be dynamical and no more statically hardcoded.

Unfortunately there is no (standard) way you can directly access the section index subview. However, you can use the methods for calculating the CGSize of text to determine dynamically the width of the section index.
You could determine all possible strings to be returned by sectionIndexTitlesForTableView: beforehand and calculate the necessary size, perhaps padding with some extra pixels left and right. Maybe it is necessary to make some experiments to determine the correct font and size.
Now your approach with something like screenSize.width - textSize.width - 2*PADDING should be viable. The only hardcoded thing is now the padding, which should not break things when new screen sizes are introduced.

Ok, to save other people a few hours of work....I laboriously tested various fonts and sizes and margins, comparing them to a UIView hierarchy dump of an actual table view with an index, to arrive at this method which will return the width of a table index, so that the bounds of the table view cell content can be calculated as table_width - index_width. I will point out that there is often another 20 pixel right-side amount reserved for an accessory view.
I also discovered that the cell.contentView.bounds is NOT correctly set until AFTER cellForRowAtIndexPath and willDisplayCell methods are called, so trying to grab the value during those calls is doomed to fail. It is set correctly by the time viewDidAppear is called.
I have no idea how to calculate the index width if the device language is set for a non-English alphabet.
- (CGFloat) getMaxIndexViewWidth
{
CGFloat indexMargin = 21.0;
NSArray *sectionTitles = [self sectionIndexTitlesForTableView:nil]; //NOTE -- if multiple tables, pass real one in
CGFloat maxWidth11 = CGFLOAT_MIN;
for(NSString *title in sectionTitles)
{
CGFloat titleWidth11 = [title sizeWithFont:[UIFont fontWithName:#"Helvetica-Bold" size:11.0f]].width;
maxWidth11 = MAX(maxWidth11, titleWidth11);
}
return maxWidth11+indexMargin;
}

Related

Infinite Scrolling UITableView is glitchy when scrolling up after retrieving data from server

I have a feed that gets populated with 15 posts from the server. When I scroll down to 3 before the end of the list, I ping the server for the next 15 posts. This functionality works great. However, when I start scrolling up, the UITableViewCells frequently jump up, as though Cell 5 is now populating Cell 4, and Cell 4 is now populating Cell 3, etc. Either that, or the UITableView scroll is just jumping up.
When I get to the very top of the UITableView and then proceed to scroll down through all my data then back up, it works perfectly though. Is there a drawing issue with my table?
Edit: So, I've come across the understanding that this is happening because the heights of all my cells are dynamic. I'm pretty sure as I'm scrolling up, my UITableView is calculating and setting the appropriate heights, which is causing the jumpy action. I'm not sure how to mitigate that.
I never used the new funcionality of dynamic cell size in iOS8, but I can give you few suggestion for improve performance on table views. It should be a comment but it doesn't fit.
Cache the height of cells already displayed if you can. It's easy an dictionary paired with a sort of id would do the trick
Pay attention that you do not have complex layout between subviews of you cells
Check if you are drawing something that requires offscreen rendering, such as corner radius, clipping etc
I don't know how dynamic cell works on ios8 but I share piece of my code. It's pretty straightforward. I have a cell that I use as prototype, each times I need to calculate a cell height I feed it with my data, that I force it's layout to get me the correct height. Once I've got the height I saved it in a NSDictionary using the postID(it's a twitter like app) as a key.
This happens only when the cell height is not cached. If it is cached the height is returned.
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGSize size = CGSizeZero;
NSDictionary * data = self.timelineData[indexPath.row];
if (data[KEY_CELL_IDENTIFIER] == CellIdentifierPost) {
NSNumber * cachedHeight = [self.heightCaches objectForKey:#([(AFTimelinePostObject*)data[KEY_CELL_DATA] hash])];//[(AFTimelinePostObject*)data[KEY_CELL_DATA] timelinePostObjectId]];
if (cachedHeight) {
return (CGFloat)[cachedHeight doubleValue];
}
[_heightCell configureCellWith:data[KEY_CELL_DATA]];
size = [_heightCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
[self.heightCaches setObject:#(size.height) forKey:#([(AFTimelinePostObject*)data[KEY_CELL_DATA] hash])];//[(AFTimelinePostObject*)data[KEY_CELL_DATA] timelinePostObjectId]];
}
else if (data[KEY_CELL_IDENTIFIER] == CellIdentifierComment){
NSNumber * cachedHeight = [self.heightCaches objectForKey:#([(AFTimelinePostComments*)data[KEY_CELL_DATA] hash])];//[(AFTimelinePostObject*)data[KEY_CELL_DATA] timelinePostObjectId]];
if (cachedHeight) {
return (CGFloat)[cachedHeight doubleValue];
}
[_heightCommentCell configureCellWith:data[KEY_CELL_DATA]];
size = [_heightCommentCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
if (size.height < 80.0f) {
size = (CGSize) {
.width = NSIntegerMax,
.height = 115.f
};
}
else if (size.height > 180.0f) {
size = (CGSize) {
.width = NSIntegerMax,
.height = 180.f
};
}
[self.heightCaches setObject:#(size.height) forKey:#([(AFTimelinePostComments*)data[KEY_CELL_DATA] hash])];//[(AFTimelinePostObject*)data[KEY_CELL_DATA] timelinePostObjectId]];
}
else {
size = (CGSize) {
.width = NSIntegerMax,
.height = 50.f
};
}
return size.height;
}
Unfortunately, there does not seem to be a simple answer to this. I have struggled with it on multiple iOS apps.
The only solution I have found is to programmatically scroll to the top of your UITableView once it appears again.
[self.tableView setContentOffset:CGPointMake(0, 0 - self.tableView.contentInset.top) animated:YES];
OR
self.tableView.contentOffset = CGPointMake(0, 0 - self.tableView.contentInset.top);
Hope this an acceptable work around while still being able to use dynamic cell heights =)

UICollectionViewFlowLayout performance for Text Grid

I have an iOS 7 app using Storyboards that has the following structure:
UITabBarController->UINavgiationController->UICollectionViewController
I use a custom subclass of UICollectionViewFlowLayout to layout the grid. Each cell contains a single UILabel.
I am trying to render a 5 column grid in the UICVC. There can be upwards of 800 rows (sections, as one row per section). I've based this loosely on Erica Sadun's example code from her iOS book.
The cells all have specific, set widths (2 different widths used). All cells have the same height. The grid is wider than the physical display, and so scrolls horizontally and vertically.
It's all working fine, but performance has been very poor for more than about 30 rows. The problem comes when trying to calculate the custom layout, specifically in the following method:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attribs = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *attributes = [NSMutableArray array];
[attribs enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *currentLayout, NSUInteger idx, BOOL *stop)
{
NSString *layoutItemKey = [NSString stringWithFormat:#"%ld:%ld", (long)currentLayout.indexPath.section, (long)currentLayout.indexPath.item];
UICollectionViewLayoutAttributes *newLayout = [self.cachedLayoutAttributes objectForKey:layoutItemKey];
if (nil == newLayout)
{
newLayout = [self layoutAttributesForItemAtIndexPath:currentLayout.indexPath];
long section = currentLayout.indexPath.section;
long item = currentLayout.indexPath.item;
NSString *layoutItemKey = [NSString stringWithFormat:#"%ld:%ld", (long)section, (long)item];
[self.cachedLayoutAttributes setObject:newLayout forKey:layoutItemKey];
}
[attributes addObject:newLayout];
}];
return attributes;
}
The main delay here seems to come from a call to the iOS method:
layoutAttributesForCellWithIndexPath:(NSIndexPath *)indexPath;
which is inside the layoutAttributesForItemAtIndexPath method:
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGSize thisItemSize = [self sizeForItemAtIndexPath:indexPath];
CGFloat verticalOffset = [self verticalInsetForItemAtIndexPath:indexPath];
CGFloat horizontalOffset = [self horizontalInsetForItemAtIndexPath:indexPath];
if (self.scrollDirection == UICollectionViewScrollDirectionVertical)
attributes.frame = CGRectMake(horizontalOffset, verticalOffset, thisItemSize.width, thisItemSize.height);
else
attributes.frame = CGRectMake(verticalOffset, horizontalOffset, thisItemSize.width, thisItemSize.height);
return attributes;
}
Scrolling works fine to a point, then freezes for about 1.5 seconds while the next block of layout is calculated (always seems to be about 165 cells). As I'm caching everything, the next time the user scrolls performance is fine.
If I leave the cell widths to the UICollectionViewFlowLayout default everything flies, with no pauses.
To try to speed things up I have:
Ensured all views are opaque
Set the deceleration rate of the CollectionView to FAST
Made - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds return NO
I cache any UICollectionViewLayoutAttributes calculated
I cache the first 50 rows on initialisation. There's no noticeable lag doing this, and it allows the initial scroll performance to be a bit better than it would have been
I've run out of ideas for squeezing more performance out of the UICollectionViewFlowLayout.
Can anyone suggest how I can improve the code?
Thanks
Darren.
I've found a solution to the problem, and while it's not a complete answer to UICollectionView performance problems, it meets my needs.
I'm rendering a text grid. The rows are all the same height, the widths of the fields are always the same, and the overall width of a row is always the same.
Erica Sadun's code allows for variable height text cells, for text wrapping I assume. Because of that the layout needs to be calculated for each cell and row. For a grid with more than 100 cells the amount of time required is too long (on my iPhone 4S).
What I've done is to remove all the height and width calculation code, and replace it with fixed values I calculated manually.
This removes the bottleneck, and now I can render a grid with a few thousand cells without any noticeable lag.

iOS - my heightForRowAtIndexPath implementation causes performance issues, why?

I use a table view where I override heightForRowAtIndexPath. When I run the Xcode profiler I found the following:
I want to calculate the height based one object's property. I use the same table view for two kind of objects, User and Post. My implementation currently looks like this:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *text;
Post *cellPost;
User *cellUser;
if (_pageType == FOLLOWERS || _pageType == FOLLOWING) {
cellUser = [self.users objectAtIndex:indexPath.row];
text = cellUser.userDescription.text;
if (text == nil) {
return 70;
}
} else {
cellPost = [self.fetchedResultsController objectAtIndexPath:indexPath];
text = cellPost.text;
}
CGSize boundingSize = CGSizeMake(245, CGFLOAT_MAX);
CGSize requiredSize = [text sizeWithFont:[UIFont fontWithName:#"TisaMobiPro" size:15]
constrainedToSize:boundingSize
lineBreakMode:NSLineBreakByWordWrapping];
CGFloat textHeight = requiredSize.height;
CGFloat cellHeight = textHeight + 40;
if ([cellPost.text isEqualToString:self.post.text]) {
cellHeight += 44;
if (cellHeight < 114) {
cellHeight = 114;
}
} else {
if (cellHeight < 70) {
cellHeight = 70;
}
}
if (cellPost.repostedBy != nil && cellPost.youReposted.boolValue == NO && _pageType != CONVERSATION) {
cellHeight += 27;
}
return cellHeight;
}
If I remove most of the code and only have, e.g. return 100 the tableView's scrolling performance is much improved. Can you spot or give suggestions on something in my implantation that could cause this performance issue?
Heights are calculated before the – tableView:cellForRowAtIndexPath: is called and for all the number of cells.
If you are deploying on iOS7 you can use the - tableView:estimatedHeightForRowAtIndexPath:, using that method you can defer this calculation while scrolling the tableview. That means that the method tableView:heightForRowAtIndexPath: is called basically when a cell is displayed. The estimation could be a fixed number close to the average of your TVC or based on some math faster than the one that you implemented.
Thanks to the estimation the TV can set its content size an scroll bar height and doesn't need to calculate each height for 100 of rows in one shot. The payback could be a lag during scrolling because the TV is calculating the actual row height.
If you are targeting iOS7 you can use the NSTableView property: estimatedRowHeight.
From the Apple docs:
Providing a nonnegative estimate of the height of rows can improve the
performance of loading the table view. If the table contains variable
height rows, it might be expensive to calculate all their heights when
the table loads. Using estimation allows you to defer some of the cost
of geometry calculation from load time to scrolling time.
Also:
Every time a
table view is displayed, it calls tableView:heightForRowAtIndexPath:
on the delegate for each of its rows
If you have to use tableView:heightForRowAtIndexPath: cache the heights as #Waine suggests.

how calculate UITableViewCell height seeing content

I need to determine cell height and place in this cell text which has images inside, for example:
some text there
image
sometext there
image
... ect.
How to implement this? Suggest pls.
Thnx.
And I want divide my paragraphs in individual cells like this:
Because the content is dynamic, one way to do it is to calculate the height of the cell ahead of heightForRowAtIndexPath (or equivalent method) being called and store those values in an array.
-(void)calculateCellHeights:(NSArray *)contentArray
{
self.heightArray = [[NSMutableArray alloc] initWithCapacity: contentArray.count];
for( MyCellContent *content in contentArray )
{
CGFloat totalHeight = 0;
totalHeight += content.image.size.height;
CGSize titleSize = [content.title sizeWithFont:self.myTitleFont];
totalHeight += titleSize.height;
CGSize textContentSize = [content.textContent sizeWithFont:self.myTextContentFont];
totalHeight += textContentSize.height;
[self.heightArray addObject:#(totalHeight)];
}
}
-(CGFloat) tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*) indexPath
{
return self.heightArray[[indexPath.row floatValue]];
}
This is assuming only one section and the use of regular text strings instead of NSAttributedStrings, and doesn't handle padding between interface elements but you get the picture.

Display UITableViewController in UIPopoverController with variable width

I will use a UIPopoverController to display a UITableViewController. The table view will show a list of names with unknown lenghts (could be Bob or could be Alexander).
I know how to vary the height of the popover, but i can't seem to figure how to vary the width of it so a name does not get truncated.
Need to figure out the width of a name so i can set the popover to that width so names don't bleed out of the popover:
any ideas?
Get the width of the string and then set the UITable to be that width. Here is a method you could add to get the width. You could add it to a category or to your class. I took the code from here.
- (CGFloat)widthOfString:(NSString *)string withFont:(NSFont *)font {
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, nil];
return [[[NSAttributedString alloc] initWithString:string attributes:attributes] size].width;
}
This way you can specify the font you are using in case you end up changing it in the future. (Different fonts obviously will result in different widths).
Presumably you've got any array of names. What you'll do is enumerate the list of names, determine the lengths required for each one, and then store the greatest one and apply it as the desired width. You will need to define a "maximum size" that should not be exceeded. A method along these lines should return something suitable:
- (CGFloat)suitableWidthForLabel:(UILabel *)nameLabel withNames:(NSArray *)arrayOfNames
{
CGFloat suitableWidth = 100.0f; // A reasonable starting point
CGSize maximumSize = CGSizeMake(600.0f, nameLabel.frame.size.height);
UIFont *labelFont = [nameLabel font];
for (NSString *aName in arrayOfNames)
{
CGSize expectedLabelSize = [aName sizeWithFont:labelFont
constrainedToSize:maximumSize
lineBreakMode:nameLabel.lineBreakMode];
suitableWidth = MAX(suitableWidth, expectedLabelSize.width);
}
return suitableWidth; // This will be the largest size you may need to accommodate
}

Resources