I've never found a way to avoid duplicating size calculations in code. Here's what I usually do:
Getting the size in the layout:
The layout can talk to its collection view to find sizes (as well as things like minimum inter-item spacing, etc.). You can do something like this in -prepareLayout
or -layoutAttributesForItemAtIndexPath:
:
if ([self.collectionView.delegate respondsToSelector:@selector(collectionView:layout:sizeForItemAtIndexPath:)]) {
id<UICollectionViewDelegateFlowLayout> delegate = (id<UICollectionViewDelegateFlowLayout>) self.collectionView.delegate;
size = [delegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath];
}
else {
size = self.itemSize;
}
Now, your collection view delegate just needs to be able to respond to collectionView:layout:sizeForItemAtIndexPath:
, which would have been necessary even without a custom layout class.
Calculating the size in the first place:
It is a little troublesome to provide correct sizes sometimes. There's no magical way to get the size of a cell without actually instantiating it and laying it out, so I always end up writing that code myself. Most of my collection view cells have a class method:
+ (CGSize)sizeWithItem:(id<MyDisplayableItem>)item;
I call that from my collection view delegate when I need the size a cell will take:
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
Note* note = self.notes[indexPath.item];
CGSize result = [CSDetailNoteCell sizeWithNote:note];
// or [MyCell sizeWithItem:myItem andMyOtherItem:somethingElse]
return result;
}
Unfortunately, the +sizeWithItem:
method doesn't come for free, but at least it keeps the size calculation encapsulated within the Cell class. I tend to store a constant in the class representing the size of things that never change (padding, maybe an image). Then, I combine that fixed size with the things that need to be calculated dynamically to find the final size. The dynamic stuff usually includes some calls to NSString's -boundingRectWithSize:options:attributes:context:
, etc.
Sample size calculation:
This example uses a cell whose width is always 320, but I think it's enough to get the point across. This is code from CSDetailNoteCell.m
.
static CGFloat const CSDetailNoteCellWidth = 320.f;
static CGFloat const CSDetailNoteCellHeightConstant =
10 + // top padding
16 + // date label height
0 + // padding from date label to textview
8 + // textview top padding
8 + // textview bottom padding
22 // bottom padding
;
...
+ (CGFloat)textWidth {
return CSDetailNoteCellTextViewWidth - 10; // internal textview padding
}
+ (CGSize)sizeWithNote:(CSNote *)note {
CGFloat textHeight = [self _textHeightWithText:note.text];
CGSize result = CGSizeMake(CSDetailNoteCellWidth, CSDetailNoteCellHeightConstant + textHeight);
return result;
}
+ (CGFloat)_textHeightWithText:(NSString*)text {
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
CGSize maxSize = CGSizeMake(CSDetailNoteCell.textWidth, CGFLOAT_MAX);
NSDictionary* attributes = @{NSFontAttributeName: CSDetailNoteCell.font, NSParagraphStyleAttributeName: paragraphStyle};
CGRect boundingBox = [text boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
CGFloat textHeight = ceilf(boundingBox.size.height);
return textHeight;
}