The UITableViewCell's expansion with long long content in iOS - ios

1.When the content inside the cell is too long, cell expansion, Or then shrink, UITableView will scroll to the back of the cell position.
2.I want cell to roll back to where it started expansion.
my code:
((PartnershipsTableViewCell *)cell).commentSpreadButtonClickHandler = ^() {
// just call - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
[weakSelf.tableView beginUpdates];
[weakCell configUI];
[weakSelf.tableView endUpdates];
// if here use "[weakSelf.tableView reloadData]",
// it can be correct,
// Unless on the first cell which have the expansion button.
};
then update uitableview cell's height. but the result isn't what i want
- (void)configUI {
if ([self.baseModel isKindOfClass: [UserWorldDynamicModel class]]) {
self.model = (id)self.baseModel;
}
[self setupValue];
}
- (void)setupValue {
// setup the property value, and update the constraints with masonry
}
// the button : read less or read more
- (void)setSpreadButton {
NSString *text = self.model.isContentOpen ? #"read less" : #"read more";
if (!self.spreadButton) {
self.spreadButton = [MYSUtil createButtonWithTitle: text target: self sel: #selector(spreadButtonClick:) image: nil font: Font14 color: DarkBlueTextColor cornerRadius: 0];
self.spreadButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
}
if (self.model.shouldShowSpreadButton) {
if (!self.spreadButton.superview) {
[self.whiteBackgroudView addSubview: self.spreadButton];
}
[self.spreadButton setTitle: text forState: UIControlStateNormal];
self.spreadButton.selected = [self.model.isSpreadState intValue];
self.spreadButton.frame = CGRectMake(CGRectGetMinX(self.contentLabel.frame), CGRectGetMaxY(self.contentLabel.frame), 80, 30);
self.tempView = self.spreadButton;
} else {
if (self.spreadButton.superview) {
[self.spreadButton removeFromSuperview];
}
}
}
// calculate the height of the label and compare it with the fixed value
NSMutableAttributedString *string = [self createMutableAttibuteStringWithNSString: text withFont: font];
self.contentLabel.attributedText = string;
// here calculate maxContentLabelHeight with
CGFloat maxContentLabelHeight = self.contentLabel.font.pointSize * (numberOfLines + 1) + 16;
// here calculate the NSMutableAttributedString's height
YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(width, MAXFLOAT)];
YYTextLayout *textLayout = [YYTextLayout layoutWithContainer:container text: string];
CGSize size = textLayout.textBoundingSize;
CGFloat height = size.height;
// then compare the NSMutableAttributedString's height with the fixed value. if true, show the spreadButton
if (height > maxContentLabelHeight) {
self.model.shouldShowSpreadButton = YES;
// storage the real height and temp height, use to calculate the tableView's contentOffset, when cell from expansion state to shrinking state in block.
self.model.contentHeight = height;
self.model.tempContentHeight = maxContentLabelHeight;
}
// if height > maxContentLabelHeight and the property "isContentOpen" of the viewModel, the height value is maxContentLabelHeight, Or not, the height value is height
if (!self.model.isContentOpen && height > maxContentLabelHeight) {
height = maxContentLabelHeight;
}
// no matter cell is expansion state or shrinking state, reset label's frame.
self.contentLabel.frame = CGRectMake(x, CGRectGetMaxY(self.headerImageView.frame) + Margin_Top, width, height);

readMore/ readLess block
before tableView reloadData on mainQueue, record it's contentOffset, Used to calculate the position of the tableView need to scroll. like this:
CGPoint point = weakSelf.tableView.contentOffset;
reloadData : refresh tableView On mainQueue.
when reloadData complete, scroll tableView to the position which expanded. when tableView from expansion state to shrinking state, and the height of expansion state is greater than 70% of the Screen's height, scroll the tableView (70% is ma condition, you can change is according to your condition)
- (UITableViewCell *)tableView:(UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath {
// here is your code
PartnershipsTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: cellIdentifierString];
[cell config];
((PartnershipsTableViewCell *)cell).spreadButtonClickHandler = ^() {
CGPoint point = weakSelf.tb.contentOffset;
[weakSelf.tb reloadData];
if (!baseModel.isContentOpen && baseModel.contentHeight > SCREEN_HEIGHT * 0.7) {
point.y -= baseModel.contentHeight - baseModel.tempContentHeight;
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.tb.contentOffset = point;
});
}
};
return cell;
}
- (CGFloat)tableView:(UITableView *) tableView heightForRowAtIndexPath:(NSIndexPath *) indexPath {
return 70;
}
in my issue, I find a another interesting problem, when use the follow method in your code. if your cell have great changes, especially the height of the cell. when you use the method [tableView reloadData], you'd better not use the follow method.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 70;
}
then use the method [tableView reloadData] to refresh UI, tableView will scroll to any position, so I delete it. because, this method use to estimated height of cell, then use to estimate tableView's contentSize, if the height between estimated and actual is bigger difference, use the method [tableView reloadData], will cause the tableView scroll to anywhere.(don't ask me how to know, this is a painful process for me).
I have solved the problem, leave some notes to myself, and for every one, and hope my solution can help you too.
thanks for #pckill and #Theorist, without your suggestion, I can't solve my question so perfect, thank you very much.
#Theorist I have reedited my code in my mind. Perhaps, the readability is better now.

Related

Custom UITableViewCell constraint issue on cell reuse

I have a custom UITableViewCell that has a dynamic image height. All the resizing works as its supposed to when the cell is created. The issue is once the cell is reused by the UITableView the constraint for the image height is changed and causes an “Unable to simultaneously satisfy constraints” error.
I have tried deactivating the height constraint before resetting it but it still causes the error.
With regards to setting the height anchor. This creates a new instance each time, are the deactivated constraints released from memory? Is there a way of just updating the current constraint in this format?
Should i be doing something within - (void)prepareForReuse method?
Thanks for any help
// Within Custom UITableViewCell
// The cell is a prototype cell in StoryBoard
// The width of the image is fixed and already set
// Observe when the image is set for imgView
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:#"image"]) {
if(self.imgView.image) {
CGFloat ratio = self.imgView.image.size.height / self.imgView.image.size.width;
self.imageHeightConstraint.active = NO;
self.imageHeightConstraint = [self.imgView.heightAnchor constraintEqualToAnchor:self.imgView.widthAnchor multiplier:ratio];
self.imageHeightConstraint.identifier = #"imageHeightConstraint";
self.imageHeightConstraint.active = YES;
}
}
}
The UITableViewCell auto resizes as the UIImageView is in a UIStackView which has its top and bottom anchors fixed to the top and bottom of the cells contentView.
- (void)buildUI {
UIStackView *containerStackView = [UIStackView new];
[self.contentView addSubview:containerStackView];
containerStackView.translatesAutoresizingMaskIntoConstraints = NO;
containerStackView.axis = UILayoutConstraintAxisHorizontal;
containerStackView.distribution = UIStackViewDistributionFill;
containerStackView.alignment = UIStackViewAlignmentCenter;
containerStackView.spacing = 10;
[containerStackView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:10.0].active = YES;
[containerStackView.centerXAnchor constraintEqualToAnchor:self.contentView.centerXAnchor].active = YES;
[containerStackView.widthAnchor constraintEqualToAnchor:self.contentView.widthAnchor multiplier:kImageWidthMultiplier].active = YES;
[containerStackView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-30.0].active = YES;
self.imgView = [UIImageView new];
[containerStackView addArrangedSubview:self.imgView];
self.imgView.translatesAutoresizingMaskIntoConstraints = NO;
[self.imgView.widthAnchor constraintEqualToAnchor:containerStackView.widthAnchor multiplier:0.492].active = YES;
}
It is quite complicated for calculate dynamic table view cell height. In you case, cell height depend on image height.
When table view cell is reused, all cell's properties will be reused and the height is not exception. The easy way to make change is using heightForRowAtIndexPath, but it very hard when using autolayout to calculate height.
According this article (http://www.raywenderlich.com/87975). The subview in table view cell will be compressed and after that, we could easy to get height and return in heightForRowAtIndexPath. However, it take a little time to calculate and make your scroll is not smooth, but we could using estimatedRowHeight, your tableview will use the estimate height and apply for specific row before we can calculated exactly height for this row
The below is short demo code:
//Estimate height (height could be 35, 42, 50, 29, etc)
//Just estimate a number
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0) {
return 40.0f;
}
//Your exactly height for row
- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell {
[yourCustomCell setNeedsLayout];
[yourCustomCell layoutIfNeeded];
CGSize size = [yourCustomCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//only set once
static YourCustomTableViewCell *yourCustomCell = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//load custom cell
yourCustomCell = (YourCustomTableViewCell*)[Utils getCellNibFile:#"YourCustomTableViewCell" Owner:self];
});
//get image from datasource
UIImage* image = [self.dataSource objectAtIndex:indexPath];
//set image for cell and begin calculate height after set image
[yourCustomCell setImage:image];
[self calculateHeightForConfiguredSizingCell:yourCustomCell];
return height;
}

ios8 UITableView scrolling jumps back when scrolling up

I have an odd situation with my UItableView.
I am loading images from the internet and presenting them in a uitableviewcell in an asynchronous manner. When I scroll down (i.e. row 1 to 6) the scroll scrolls down smoothly just as I expect it to.
However, then I scroll up (i.g. row 6 to 1) the scroll jumps back. for example if I scroll from row 6 to row 5, it will jump back to row 6. The second time I try to scroll up it lets me go up to row 4, but then it scrolls me back to row 5 or 6 but usually just 5.
What I don't understand is why this is happening in only one direction.
It seems to be effecting ios 8 but not ios 7.
Therefore it is an implementation difference in how ios8 and 7 handle the uitableview.
How can I fix this?
Here is some code to give you some context
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CVFullImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
CVComicRecord *comicRecord = self.comicRecords[indexPath.row];
[cell.loaderGear stopAnimating];
[cell.text setText:comicRecord.title];
NSString *UUID = comicRecord.fullImagePageURL.absoluteString;
[cell setUUID:UUID];
//Check if image is in the cache
UIImage *fullImage = [self.contentViewCache objectForKey:UUID];
[cell setComicFullImage:fullImage];
if (fullImage) {
return cell;
}
[cell.loaderGear startAnimating];
[self requestImageForIndexPath:indexPath];
return cell;
}
- (void)requestImageForIndexPath:(NSIndexPath *)indexPath {
CVComicRecord *comicRecord = self.comicRecords[indexPath.row];
NSString *UUID = comicRecord.fullImagePageURL.absoluteString;
if ([self.contentViewCache objectForKey:UUID]) {
//if it is already cached, I do not need to make a request.
return;
}
id fd = CVPendingOperations.sharedInstance.fullDownloadersInProgress[UUID];
if (fd) {
//if it is in the queue you do no need to make a request
return;
}
comicRecord.failedFull = NO;
CVFullImageDownloader *downloader = [[CVFullImageDownloader alloc] initWithComicRecord:comicRecord withUUID:UUID];
[CVPendingOperations.sharedInstance.fullDownloaderOperationQueue addOperation:downloader];
//when operation completes it will post a notification that will trigger an observer to call fullImageDidFinishDownloading
}
- (void)fullImageDidFinishDownloading:(NSNotification *)notification {
CVComicRecord *comicRecord = notification.userInfo[#"comicRecord"];
NSString *UUID = notification.userInfo[#"UUID"];
UIImage *fullImage = notification.userInfo[#"fullImage"];
comicRecord.failedFull = NO;
[self.contentViewCache setObject:fullImage forKey:UUID];
dispatch_async(dispatch_get_main_queue(), ^{
for (NSIndexPath *indexPath in [self.tableView indexPathsForVisibleRows]) {
CVFullImageTableViewCell *cell = (id)[self.tableView cellForRowAtIndexPath:indexPath];
if (cell) {
if ([cell.UUID isEqualToString:UUID]) {
[cell.loaderGear stopAnimating];
[cell setComicFullImage:fullImage];
[cell layoutIfNeeded];
}
}
}
});
}
#pragma mark - Scroll
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
static int oldOffset = 0;
int newOffset = scrollView.contentOffset.y;
int dy = newOffset- oldOffset;
if (dy > 0) {
[self hideNavigationbar:YES animationDuration:0.5];
} else if (dy < 0) {
[self hideNavigationbar:NO animationDuration:0.5];
}
oldOffset = newOffset;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (decelerate == NO) {
[self prioritizeVisisbleCells];
//currentPage is a property that stores that last row that the user has seen
[self setCurrentPage:[self currentlyViewedComicIndexPath].row];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self prioritizeVisisbleCells];
//currentPage is a property that stores that last row that the user has seen
[self setCurrentPage:[self currentlyViewedComicIndexPath].row];
}
- (void)prioritizeVisisbleCells {
NSArray *ips = [self.tableView indexPathsForVisibleRows];
NSArray *activeIndexPaths = [CVPendingOperations.sharedInstance.fullDownloadersInProgress allKeys];
//add visible cells to queue first
NSSet *visible = [NSSet setWithArray:ips];
NSMutableSet *invisible = [NSMutableSet setWithArray:activeIndexPaths];
[invisible minusSet:visible];
for (NSIndexPath *ip in invisible) {
NSOperation *op = CVPendingOperations.sharedInstance.fullDownloadersInProgress[ip];
[op setQueuePriority:NSOperationQueuePriorityNormal];
}
for (NSIndexPath *ip in visible) {
NSOperation *op = CVPendingOperations.sharedInstance.fullDownloadersInProgress[ip];
[op setQueuePriority:NSOperationQueuePriorityHigh];
}
}
I found the answer in this post very helpful: Table View Cells jump when selected on iOS 8
Try to implement the tableView:estimatedHeightForRowAtIndexPath: in the UITableViewDelegate protocol. It works for me.
Implement the self-sizing and correct view constraints in your storyboard.
in your code put this together
tableView.rowHeight = UITableViewAutomaticDimension;
tableView.estimatedRowHeight = CGFloat value (the initial value)
This exact problem was happening to me while I was using the UITableViewAutomaticDimension and the estimatedRowHeight, as #nferocious76 suggested.
The issue was that my estimatedRowHeight was 400.0 while the actual row height was more like 80.0 (I copy/pasted it from another table view in my app with large cells). I never bothered to change it because I had read somewhere that the estimatedRowHeight didn't really matter as long as it was greater than the actual row height, but apparently this is not the case.
You should calculate your row height yourself and you should remove everything about estimated row height in your code.

Table Cell expansion laggy

I have a custom table view & cell where a cell is expanded when selected. It is now functioning properly and accordingly. However, When I select cells to expand them, it takes about half a second to respond. The code below is located in the didSelectRowAtIndexPath. My hypothesis is that between beginUpdates and endUpdates, there are too many things going on to increase the height of the original cell and then updating the whole table view. Is there another way I can better implement this?
**[_tableView beginUpdates];**
ReviewTestTableCell *reviewCell1 = (ReviewTestTableCell *)[tableView cellForRowAtIndexPath:indexPath];
CGRect rect = CGRectMake(reviewCell1.review.width, 900, reviewCell1.review.width, 900);
CGRect textRectb = [reviewCell1.review textRectForBounds:rect limitedToNumberOfLines:1000];
float labelHeight = textRectb.size.height;
reviewCell1.review.height = labelHeight;
expandHeight = labelHeight + 75 ;
if ([[userdefaults objectForKey:#"merchantType"] isEqual:#"T"])
{reviewCell1.height = labelHeight + 50;
reviewCell1.bottomRatingView.height = reviewCell1.bottomRatingView.height - 20;
}
else
{
reviewCell1.height = labelHeight + 75;}
reviewCell1.bottomRatingView.hidden = NO;
reviewCell1.bottomRatingView.top = reviewCell1.review.bottom;
**[_tableView endUpdates];**
[_isExpandList replaceObjectAtIndex:indexPath.row withObject:#1];
}
EDIT/ADD:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell3 = [self tableView:tableView cellForRowAtIndexPath:indexPath];
if ([_isExpandList[indexPath.row] isEqual: #1] && [[_dataList objectAtIndex:indexPath.row] valueForKey:#"Item1Score"] != nil) {
return cell3.height + 3 + 65;
}
else if ([_isExpandList[indexPath.row] isEqual: #0])
{
return cell3.height +5;
}
else return cell3.height +3;
}
You should not go through that elaborate dance of trying to manually expand the cell. You should certainly not manually call willDisplayCell.
Using the method described in the answer to this question, you should have a property or something to keep track of which cell was selected and make your heightForRowAtIndexPath: method adjust for that particular indexPath, then just call
[tableView beginUpdates];
[tableView endUpdates];
Which will call heightForRowAtIndexPath for every cell, which your method will give a larger height for when it matches the selected row. The tableView will smoothly adjust the height of your cell.
Something similar to:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.selectedIndexPath isEqual:indexPath]) {
return 80.0f;
}
return tableView.rowHeight;
}

Adjusting elements and the height of UITableView on scroll

I have a UITableView with multiple UILabels. The issue is that the text in these cells change dynamically as I receive data from the server. It works fine when I load the view controller. But as I scroll, the height of the cells are not updated as heightForRowAtIndexPath is only called once.
Here are the screenshots:
As I've shown in the screenshot, the question label reduces in size which leads to a gap (shown by arrow).
Here's my cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIndentifier = #"CustomCell";
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIndentifier];
if (cell == nil)
{
cell = [[CustomCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIndentifier];
}
cell.question.autoDetectLinks = YES;
// Used to populate cell from NSDictionary
[self setDataToCell:cell AtIndexPath:indexPath];
return cell;
}
Here's my custom cell's layoutSubviews:
- (void) layoutSubviews
{
CGRect frame = self.question.frame;
frame.size.width = 277.0f; //you need to adjust this value
self.question.frame = frame;
self.question.numberOfLines = 2;
[self.question sizeToFit];
// Place time below question
CGRect timeFrame = self.time.frame;
timeFrame.origin.y = self.question.frame.origin.y + self.question.frame.size.height + 5;
self.time.frame = timeFrame;
[self.time sizeToFit];
}
So to tackle this situation I called
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[_tableIndexPath] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
in - (void) scrollViewDidScroll:(UIScrollView *)scrollView
This solves my problem but reduces the performance and the elements jump around before settling even after setting the animation as UITableViewRowAnimationNone. Is there a better way of doing it? Should I call reloadRowsAtIndexPaths somewhere else?
Thanks.
Ok here come the edited answer:
The problem is your sizeToFit in your layoutSubviews. Just follow this link to resolve your issue:
here
If you also want the cell and the label to dynamically resize to their corresponding text but at the same time your timelabel to be directly underneath it you will have to determine the size of your uilabel based on the text, font and font-size.
See here for more information:
here

Resize tableview NSLayoutConstraint

Hi I have a tableView which I change the width programmatically by his constraint:
self.widthTableLeft.constant = self.view.frame.size.width;
I do this in the viewDidLoad.
In the delegate method:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
I need the contentview frame of each cell to calculate the hight of the cell, but when get it, this is the old size, I mean the size if I wouldn't have done the resize.
When the table is showed the size of the table is right but the hight of the cell is wrong.
I have tried to call:
[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
The question is, before to show the view of my view controller, when is the best place to update the width constraint of a UITableView and to get the right contentView in the cells in the method heightForRowAtIndexPath.
Sorry but I'm lost, could you give me any ideas?
Thanksssss
UPDATE
This is the code in heightForRowAtIndexPath
static const NSInteger ROC_TITLE_LABEL_TAG = 2;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
ROCAgendaItemDetailEntity *agendaItemDetail = [self.listRight objectAtIndex:indexPath.row];
UITableViewCell *cell = [self.tableLeft dequeueReusableCellWithIdentifier:#"cellVideo"];
//I get the label which will make the cell bigger
UILabel *titleLabel = (UILabel *)[cell viewWithTag:ROC_TITLE_LABEL_TAG];
//We init the height title with the min height.
float extraSpaceHeightLabel = 0;
if (agendaItemDetail.agendaItem.title.length > 0) {
//I will use his width to get the hight that I need to write all text
float width = titleLabel.frame.size.width;
CGSize size = CGSizeMake(width, CGFLOAT_MAX);
//we get the size
CGSize sizeTitleLabel = [agendaItemDetail.agendaItem.title sizeWithFont:[ROCTextsInformation imagoMed:15] constrainedToSize:size lineBreakMode:titleLabel.lineBreakMode];
//we substract the current height of the label to know the extra space to write all text
sizeTitleLabel.height -= titleLabel.frame.size.height;
extraSpaceHeightLabel = sizeTitleLabel.height;
}
//The final hight will be the contenViewHeight pluss the extra space needed.
return cell.contentView.frame.size.height + extraSpaceHeightLabel;
}
and this is the cell in the table view
Thanks again
viewDidLoad is too early to try and access the geometries of your views. Instead, access self.view's width in viewDidLayoutSubviews.
#interface CustomViewController ()
#property (nonatomic) BOOL isFirstTimeViewDidLayoutSubviews; // variable name could be re-factored
#end
#implementation CustomViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.isFirstTimeViewDidLayoutSubviews = YES;
}
- (void)viewDidLayoutSubviews
{
// only after layoutSubviews executes for subviews, do constraints and frames agree (WWDC 2012 video "Best Practices for Mastering Auto Layout")
if (self.isFirstTimeViewDidLayoutSubviews) {
// execute geometry-related code...
self.widthTableLeft.constant = self.view.frame.size.width;
}
self.isFirstTimeViewDidLayoutSubviews = NO;
}

Resources