UICollectionView cell borders in a flow layout - ios

I'm using UICollectionView to lay out a bunch of cells that are sectioned by first letter of their title. Each cell should have a very thin border around it, and the section headers should have borders above and below. Here's my current prototype:
I achieve the current appearance with the following rules:
Stroke the right and bottom edge of each cell.
Stroke the bottom edge of each section heading.
This is very close to what I want, but there are two defects:
If the line before a section heading isn't full, then the border along the top of the heading stops short of the right edge of the screen.
It's not visible in this screenshot, but if a line is full, the right border of the last cell in the line is still drawn, which looks a little odd against the edge of the screen.
My best idea to fix this is to somehow tell each cell if it's in the last row of a section or the last cell in a row; then the cell would turn off the offending borders, section headings would draw a top border as well as a bottom, and everything would be hunky-dory. I don't know how to achieve that, though.
Any thoughts on how to manage that, or another way to get the look I'm going for? I'm currently using a UICollectionViewFlowLayout.

I ended up subclassing UICollectionViewFlowLayout and applying several heuristics after the flow layout had calculated the attributes for each cell:
If center.y is equal to center.y of the last item in the section, the cell is in the last row of the section.
If CGRectGetMaxY(frame) is equal to CGRectGetMaxY(self.collectionView.bounds), then the cell is agains the right edge of the collection view.
I then stored the results of these calculations in a subclass of UICollectionViewLayoutAttributes, and wrote a UICollectionViewCell subclass whose -applyLayoutAttributes: method would adjust the borders its background view draws based on the additional properties.
I've put the whole mess into a fairly enormous gist so you can see exactly what I did. Happy hacking.

My best idea to fix this is to somehow tell each cell if it's in the last row of a section or the last cell in a row; then the cell would turn off the offending borders, section headings would draw a top border as well as a bottom, and everything would be hunky-dory. I don't know how to achieve that, though.
What you describe is more or less what I did in a similar scenario. I added a border property to my cell:
typedef NS_OPTIONS(NSInteger, TLGridBorder) {
TLGridBorderNone = 0,
TLGridBorderTop = 1 << 0,
TLGridBorderRight = 1 << 1,
TLGridBorderBottom = 1 << 2,
TLGridBorderLeft = 1 << 3,
TLGridBorderAll = TLGridBorderTop | TLGridBorderRight | TLGridBorderBottom | TLGridBorderLeft,
};
#interface TLGridCellView : UIView
#property (nonatomic) TLGridBorder border;
#end
Then I set the border in my view controller's cell configuration:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
TLGridCellView *cell = ...;
if (indexPath.item == self collectionView:collectionView numberOfItemsInSection:indexPath.section - 1) {
cell.border = TLGridBorderLeft;
} else {
cell.border = TLGridBorderLeft | TLGridBorderRight;
}
return cell;
}

I solve this problem in a simple way. I didn't add boarder to cell, instead I add a label with boarder into the cell. For the first column, the frame of the label is the same with the cell. For the other label, I set the x coordinate -0.5 to make their boarder overlap. Hope it helps.
Here is the code:
- (UICollectionViewCell *) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
// Then use it
UILabel *label = nil;
if (cell.contentView.subviews.count > 0) {
label = cell.contentView.subviews[0];
} else {
label = [[UILabel alloc] init];
}
label.text = #"北京";
[label setTextAlignment:NSTextAlignmentCenter];
[label setFont:[UIFont systemFontOfSize:14]];
[label setCenter:cell.contentView.center];
CGRect frame = label.frame;
if (indexPath.row%4 == 0) {
frame.origin.x = 0;
} else {
frame.origin.x = -0.5;
}
frame.origin.y = 0;
frame.size.width = self.collectionView.frame.size.width / 4;
frame.size.height = self.collectionView.frame.size.height / 9;
[label setFrame:frame];
if (cell.contentView.subviews.count == 0) {
[[cell contentView] addSubview:label];
}
label.layer.borderWidth = 0.5;
label.layer.borderColor = [[UIColor lightGrayColor] CGColor];
cell.backgroundColor = [UIColor whiteColor];
return cell;
}

Related

How to change the height of a custom cell individually within a UITableView?

I am trying to change the height of each cell individually based on how long my UILabel is, but it does not seem to change.
I have found the following question: How to change cell height dynamically in UITableView static cell
I tried the solution from that question:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//return 110;
NewsFlashCustomCell *cell;
static NSString *CellIdentifier = #"CustomCell";
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[cell layoutIfNeeded];
return cell.messageLabel.frame.origin.y + cell.messageLabel.frame.size.height;
}
However, that seems to do nothing.
Here's a screenshot:
I'd like the height of the cells containing "tidal wave" and "mashup" to be smaller than the last 2 cells since it has a longer message.
I cannot perform a indexPath.row check to change each cell individually because each message is pulled from a database, so the length of the message varies.
What can be done?
In case it helps, here's a screenshot of my constraints:
Thanks.
UPDATE:
I added the following code in viewDidLoad:
tableView.rowHeight = UITableViewAutomaticDimension;
This is what it looks like:
If you can't set try with other method reduce the label font size depend upon the cell height .
label.numberOfLines=0;
label.clipsToBounds=YES;
label.adjustsFontSizeToFitWidth=YES;
label.lineBreakMode = NSLineBreakByWordWrapping;
You can calculate height for label from its length of text and do it like following way:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return [self calculateHeightForCellAtIndexPath:indexPath];
}
#pragma mark - cell height handling
-(CGFloat)calculateHeightForCellAtIndexPath:(NSIndexPath *)indexP{
UILabel *lbl = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, self.tableView.frame.size.width-22, defaultHeightOfYourLabel)];
NSString *strLbl;
strLbl = [yourArray objectAtIndex:indexP.row];
lbl.text = strLbl;
if (IS_IPAD) {
[lbl setFont:fontRegular15];
}else{
[lbl setFont:fontRegular12];
}
UIFontDescriptor * fontDLBLTime = [lbl.font.fontDescriptor
fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
if (IS_IPAD) {
[lbl setFont:[UIFont fontWithDescriptor:fontDLBLTime size:15]];
}else{
[lbl setFont:[UIFont fontWithDescriptor:fontDLBLTime size:12]];
}
[lbl setLineBreakMode:NSLineBreakByClipping];
[lbl setNumberOfLines:0];
int noOfLines = [GlobalFunction lineCountForLabel:lbl];
CGFloat height = [GlobalFunction heightForWidth:lbl.frame.size.width usingFont:lbl.font forLabel:lbl];
if (noOfLines>1) {
return height + 33; //here 33 is sum of top and bottom space from view.
}
return 50; //default height of cell.
}
In your class implement following methods:
#define CGFLOAT_MAX_HEIGHT 2000.0
+ (int)lineCountForLabel:(UILabel *)label {
return ceil([self heightForWidth:label.frame.size.width usingFont:label.font forLabel:label] / label.font.lineHeight);
}
+(CGFloat)heightForWidth:(CGFloat)width usingFont:(UIFont *)font forLabel:(UILabel *)lbl{
NSStringDrawingContext *context = [[NSStringDrawingContext alloc] init];
CGSize labelSize = (CGSize){width, CGFLOAT_MAX_HEIGHT};
CGRect r = [lbl.text boundingRectWithSize:labelSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName: font} context:context];
return r.size.height;
}
You can customize lbl on basis of your font and label size.
Have your label constrain the bounds of your cell, like your already doing. Be sure to include padding around it so it looks nicer.
In your heightForRowAtIndexPath: calculate the height required for your label (it is important you have your label correctly configured for multiline and/or word wrapping)
Calculate the required height of label
CGRect frame = [label.text boundingRectWithSize:CGSizeMake(cell.width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName: font} context:nil];
Return the calculated height plus padding
frame.size.height + TopPadding + Bottom Padding
This is a general approach to have a dynamically sizing cell.
Note: Sometimes this is off by a few points so you may need to add up 5 extra points in the cell height to get all the text fitting in the label.
Please write following line in viewDidLoad:
_tableView.rowHeight = UITableViewAutomaticDimension;
Please make sure of following points:
Dont create height constraint of label
Make width of that label equal to screen size or content view
Apply leading, trailing, top and bottom constraint
Dont write heightForRowAtIndexPath method
Please let me know the results
EDIT :
Write following line in your cellForRowAtIndexPath method
myLabel.preferredMaxLayoutWidth = cell.contentView.frame.size.width;

Scroll UITableView Cell beyond table bounds

I have a UITableView with changing height.
I wanted in every cell selection to kind of get the 'focus' on it by moving it to the middle of the screen. The problem is to move the upper and lower cells in the table since they cannot scroll beyond the table upper and lower bounds. I'll also prefer to do this with animations..
Any suggestions?
Thanks!
I created small example of how to achieve this:
https://github.com/vkozlovskyi/CenterCellExample
You just need to calculate scroll offset of UITableView, and and animate the change:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Get offset for cell
CGFloat centerOffset = self.tableView.frame.size.height / 2 - self.tableView.rowHeight / 2;
// Content offset
CGFloat total = indexPath.row * self.tableView.rowHeight;
[UIView animateWithDuration:0.3 animations:^{
self.tableView.contentOffset = CGPointMake(0, total - centerOffset);
}];
}
It works for all cells, upper and lower too.
You can add extra space to the content of your table view using the contentInset property. Add extra space above and below your table view will allow your cells to scroll all the way to the middle of your table view.
UITableView * tableView = self.tableView ;
CGRect bounds = self.tableView.bounds ;
CGFloat extraSpaceNeeded = 0.5 * ( bounds.size.height - tableView.rowHeight ) ;
tableView.contentInset = (UIEdgeInsets){ .top = -extraSpaceNeeded, .bottom = -extraSpaceNeeded } ;
Now you can just use
[tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionMiddle
animated:animated];

Updating height of UITableViewCell

I'm building a table that shows an entry for a user. One of the cells shows a set of "tags" downloaded from the server. I am currently building a set of UILabels and manually adding them to a view contained in the cell. While this works, the cell does not dynamically resize after adding the tags. The tags overlap the cell beneath it and I can't figure out how to manually update the height of the cell.
In cellForRowAtIndexPath:
JournalTagsCell *cell = [tableView dequeueReusableCellWithIdentifier:#"JournalTagsCell" forIndexPath:indexPath];
//Check if we have any tags to show
if(self.journalObject.journalEntryTags != nil){
cell.placeholderLabel.hidden = YES;
cell.tagsView = [self updateTagsView:cell.tagsView];
}
return cell;
The following is my method for actually creating each tag, laying them out and adding them to the view:
- (void)updateTagsView:(UIView*)viewToUpdate{
NSArray *items = self.journalObject.journalEntryTags;
//Clean up the view first
NSArray *viewsToRemove = [viewToUpdate subviews];
for (UIView *v in viewsToRemove) {
[v removeFromSuperview];
}
float x = 10;
float y = 10;
for (int i = 0; i < items.count; i++) {
CGRect textRect = [items[i] boundingRectWithSize:CGSizeMake(self.view.frame.size.width - 20, 1000)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName:[UIFont tagCopy]}
context:nil];
CGSize size = textRect.size;
if (x+size.width > (self.view.frame.size.width-20)) {
y += size.height + 10;
x = 10;
}
UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(x, y, size.width, size.height)];
lbl.userInteractionEnabled = YES;
[lbl setText:[NSString stringWithFormat:#" %# ", items[i]]];
[lbl setFont:[UIFont tagCopy]];
[lbl sizeToFit];
[lbl setTextColor:[UIColor colorWithRed:0.145 green:0.392 blue:0.576 alpha:1.000]];
[lbl setBackgroundColor:[UIColor colorWithRed:0.804 green:0.871 blue:0.914 alpha:1.000]];
lbl.layer.borderWidth = 1;
lbl.layer.borderColor = [UIColor colorWithRed:0.145 green:0.392 blue:0.576 alpha:1.000].CGColor;
lbl.layer.cornerRadius = 5;
[lbl.layer setMasksToBounds:YES];
[viewToUpdate addSubview:lbl];
UITapGestureRecognizer *tapGesture =
[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(userClickedOnTag:)];
[lbl addGestureRecognizer:tapGesture];
x += size.width + 10;
if (x > (self.view.frame.size.width-20)) {
y += size.height + 10;
x = 10;
}
if (i == items.count-1) {
y+= size.height + 20;
}
}
[viewToUpdate setFrame:CGRectMake(0, 0, self.view.frame.size.width, y)];
}
However, I don't know how to manually update the height for this cell based on the size of this view. I don't want/need to manually calculate the height of every single cell, just this one which is why I'm not currently using heightForRowAtIndexPath but I don't know how to update the height of this one cell. Obviously I can calculate the height I need for this cell if necessary as I'm already setting up the view frame that holds the tags, but short of having to go through every single cell and manually calculate each ones height, I'm stumped.
I think you will have to use heightForRowAtIndexPath. I'm not sure you completely understand what's happening. Or maybe I have it wrong.. Either way, how I understand it: The total height of the "cell" will always be presented. You never set the "height" of the cell, it will automatically show the entire content. heightForRowAtIndexPath is not a way to tell the cell how tall it should be, but rather how much space the tableView should reserve for that particular cell. If you pass a height too short, it will still present the entire cell, but the next cell will start too soon. It also works the other way around, if you pass a bigger number than necessary, it will look like the cells are bigger, even though the cells aren't. It's just the tableView's representation.
If you are using iOS 8 you can use UITableViewAutomaticDimension.You can check out this example
self.tableView.rowHeight = UITableViewAutomaticDimension;
You can take a look also on this video : What's New in Table and Collection Views in the 2014 WWDC.
You can solve the issue with a variable for that row's height. In viewDidLoad() store default cell height to the variable. then while you calculate the height for the view store the view's height to the variable and to reload that cell use below method for the tableview by passing single cell's indexpath in array.
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
It will update cell at particular index by calling all lifecycle method of a cell.
Make sure you return default value for all other cell in heightForRowAtIndexPath() except the cell with tagsView with calculated height cell.
-(CGFloat)tableView:(UITableView *)myTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexpath.row == TAGSVIEW_ROWINDEX)
return calculated_height_value;
return default_row_height;
}
If the Cell with tagsView is not predefined with rowIndex, you can also store that rowIndex in one integer variable in cellForRowAtIndexPath().

UITableViewCell contentView subview presented with offset

I added small UIView at beginning of UITableViewCell's contentView (width = 6px, height = cell.frame.size.height) - this is added in storyboard.
In cellForRowAtIndexPath I reach that view via tag = 9 and set its background color (dynamic).
UIView *colorView = (UIView *)[cell viewWithTag:9];
UIColor *color = nil;
if (indexPath.section == 1)
{
// Category section - add colors here
// Get color view
color = [UIColor colorForCategory:[NewsCategory categoryForString:cellTitle]];
} else
{
color = nil;
}
[colorView setBackgroundColor:color];
That works fine but as indexpath.row is bigger (we go down in tableView) that colored view is being offset more and more down (y) where last cell is overlapped by few pixels of above's cell's color view. Dunno why that happening and its not that obvious until you press cell and new colored view is presented...

Trying to render a large columnar grid within a UITableView

I have created a sort of GRID using uitableView. For that I have taken various labels for showing grid type line I am using line image. So by considering Grid my tableview is having around 88 columns.
My issue is when I scroll it down, I am getting jerky effect. Its performance s very poor. I am creating around 108 label and each row is having 88 labels and 86 image views.
What step do I need to follow to improve scrolling performance???
I was using clearColor for label background. But later on I have removed those background colors.
Having read your problem again, I am thinking you are scrolling horizontally and vertically with an incredibly wide tableview. If this is the case then you need to switch to UIScrollView and attach each item to this view. UIScrollView will only load the views that are visible and provide the kind of scroll performance you desire.
It will be important to avoid this:
// Using ARC, therefore no release on the UILabel
- (void) viewDidLoad {
self.scrollView.backgroundColor = [UIColor whiteColor];
UIFont *font = [UIFont systemFontOfSize:24];
CGSize size = [#"10000:10000" sizeWithFont:font];
_scrollView.contentSize = CGSizeMake(10000 * size.width, 10000 * size.height);
for (int y = 0; y < 10000; y++) {
for (int x = 0; x < 10000; x++) {
NSLog(#"Loading: %d, %d", x, y);
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(x * size.width, y * size.height, size.width, size.height)];
label.font = font;
label.textAlignment = UITextAlignmentRight;
label.text = [NSString stringWithFormat:#"%d:%d", x, y];
[_scrollView addSubview:label];
}
}
}
While this will eventually load it will take ages while it loads up all of these labels and it will consume a ton of memory. You want to lazy load this view just like a TableView. I will write up an example this evening.
You need to give the table cell a reuse identifier. Otherwise, each time you create a completely new cell and consume more and more memory.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *const kReuseIdentifer = #"ReuseIdentifer";
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kReuseIdentifier] autorelease];
}
return cell;
}

Resources