How to get the frame (CGRect value) of a whole section in a UICollectionView instance?
I know that UITableView has a rectForSection: method. I am looking for a similar thing for UICollectionView.
I checked the documents for UICollectionViewFlowLayout but couldn't find anything that looks like rectForSection:.
to collectionView add category
#implementation UICollectionView (BMRect)
- (CGRect)bm_rectForSection:(NSInteger)section {
NSInteger sectionNum = [self.dataSource collectionView:self numberOfItemsInSection:section];
if (sectionNum <= 0) {
return CGRectZero;
} else {
CGRect firstRect = [self bm_rectForRowAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
CGRect lastRect = [self bm_rectForRowAtIndexPath:[NSIndexPath indexPathForItem:sectionNum-1 inSection:section]];
return CGRectMake(0, CGRectGetMinY(firstRect), CGRectGetWidth(self.frame), CGRectGetMaxY(lastRect) - CGRectGetMidY(firstRect));
}
}
- (CGRect)bm_rectForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self layoutAttributesForItemAtIndexPath:indexPath].frame;
}
#end
Collection view section frame is depend on the collection view layout.It might be flow layout or custom layout. So you will not got the any predefined solution. But you can calculate it for your layout using
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
I will suggest you to subclass UICollectionViewLayout and put you section frame calculation code inside it, so that you section frame will always be the updated one.
NOTE : These point will not give you superset of rect in case of section start point and end point not vertically aligned.To be honest there is no better way to get section frame.
Related
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.
I have UICollectionView with customLayOut, when I start scrolling,
I change x coordinate for the cell that should start to be visible - in order to be visible from the begining.
-(void)layoutSubviewsWithAttributes:(NSMutableArray *)theAttributes {
CGFloat halfScreen = [UIScreen mainScreen].bounds.size.width / 2;
CGPoint startOfScreen = CGPointMake(self.timeLineCollection.bounds.origin.x, 25.f);
for(UICollectionViewLayoutAttributes *attribute in theAttributes) {
if(CGRectContainsPoint(attribute.frame, startOfScreen)) {
NSLog(#"Intersect %#", NSStringFromCGRect(attribute.frame));
attribute.frame = CGRectMake(startOfScreen.x, startOfScreen.y, attribute.frame.size.width, attribute.frame.size.height);
}
}
but in some cases this cell appears on the top of the next one cell (wrong behaviour),
another time, that cell in under the next one cell (as I need it to be placed).
Could you advice, how to sort that cells to be placed hierarchically one on another (like cards).
Thank you in advance, for any help.
Try this with negative value it may help as i haven't tried but i am sure it should work like this.
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
return -5.0;
}
UICollectionViewLayoutAttributes has such a property as zIndex, that - Specifies the item’s position on the z axis.
So to tell cells to be set one after another, I just set to each attribute, zIndex - number of each cell and it places it one after another, inside the customLayOut.
for (NSInteger item = 0; item < self.cellCountPrograms; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:0];
UICollectionViewLayoutAttributes *itemAttributes =
[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
itemAttributes.frame = [self frameForItemAtIndexPath:indexPath];
itemAttributes.zIndex = item;
cellProgramLayoutInfo[indexPath] = itemAttributes;
}
I'm using UICollectionViewFlowLayout and wanted to apply section insets like below. All my items have the same width but varying heights. The insets work when items within the same section are the same height but not when they are different heights in the same section. Is this expected behaviour for this layout? Do I need to subclass and make a custom one or is something missing?
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
UIEdgeInsets insets = UIEdgeInsetsZero;
CGFloat big = 30;
CGFloat small = 10;
if (section < 5) {
insets = UIEdgeInsetsMake(0, big, 0, small);
} else {
insets = UIEdgeInsetsMake(0, small, 0, big);
}
return insets;
}
For some reason the UICollectionViewFlowLayout has the behaviour to center the cells if you have a single item each row. It ignores the section insets then.
You can solve this issue by overriding the UICollectionViewFlowLayout and change the following two methods:
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attribute = [super layoutAttributesForItemAtIndexPath:indexPath];
[self fixLayoutAttributeInsets:attribute];
return attribute;
}
And
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *results = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *attribute in results)
{
[self fixLayoutAttributeInsets:attribute];
}
return results;
}
The magic:
- (void)fixLayoutAttributeInsets:(UICollectionViewLayoutAttributes *)attribute
{
if ([attribute representedElementKind])
{ //nil means it is a cell, we do not want to change the headers/footers, etc
return;
}
//Get the correct section insets
UIEdgeInsets sectionInsets;
if ([[[self collectionView] delegate] respondsToSelector:#selector(collectionView:layout:insetForSectionAtIndex:)])
{
sectionInsets = [(id<UICollectionViewDelegateFlowLayout>)[[self collectionView] delegate] collectionView:[self collectionView] layout:self insetForSectionAtIndex:[[attribute indexPath] section]];
}
else
{
sectionInsets = [self sectionInset];
}
//Adjust the x position of the view, the size should be correct or else do more stuff here
CGRect frame = [attribute frame];
frame.origin.x = sectionInsets.left;
[attribute setFrame:frame];
}
This solution does not work (and is not needed) if you have multiple cells on a single row so you should check for that case with a boolean or whatever you like... This is just a simple example for this scenario
Doesn't seem to be possible with varying heights, so I made the cells full width and mange the padding logic inside the cell class itself.
Working in iOS 7, how does one specify where the header & footer boxes go in a UICollectionView?
I have a custom UICollectionViewFlowLayout. I have overwritten
-(void)prepareLayout
-(NSArray*) layoutAttributesForElementsInRect:(CGRect)rect
-(UICollectionViewLayoutAttributes*) layoutAttributesForSupplementaryViewOfKind: (NSString*)kind atIndexPath:(NSIndexPath*)indexPath
My problem is, I'm not sure how to specify header location. I have already specified that a header exists in prepareLayout:
-(void)prepareLayout
{
[super prepareLayout];
boundsSize = self.collectionView.bounds.size;
midX = boundsSize.width / 2.0f;
curIndex = 0;
self.headerReferenceSize = CGSizeMake(CELL_SIZE, TITLE_HEIGHT);
self.footerReferenceSize = CGSizeMake(0, 0);
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sectionInset = UIEdgeInsetsMake(TOP_INSET, LEFT_INSET, BOTTOM_INSET, RIGHT_INSET);
self.minimumLineSpacing = LINE_SPACING;
self.minimumInteritemSpacing = INTERIM_SPACING;
self.itemSize = CGSizeMake(CELL_SIZE, CELL_SIZE);
}
I just don't know the right property of my custom FlowLayout to set, as there doesn't seem to be something like "HeaderLocation" to set, either as a LayoutAttributes or in the layout object itself. Right now, it is appearing to the side/between my images, when I'd like them to be appearing above each image (horizontal scroll).
I have tried the following:
-(UICollectionReusableView*) collectionView: (UICollectionView*)collectionView viewForSupplementaryElementOfKind:(NSString*)kind atIndexPath:(NSIndexPath*)indexPath
{
NSLog(#"**ViewForSupplementaryElementOfKind called***");
CGFloat centerX = collectionView.center.x;
CGFloat centerY = collectionView.center.y;
CGFloat titleWidth = [MyLayout titleWidth];
CGFloat titleHeight = [MyLayout titleHeight];
MyTitleView* titleView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:ImageTitleIdentifier forIndexPath:indexPath];
titleView.frame = CGRectMake(centerX - titleWidth/2.0,
0.0,
titleWidth,
titleHeight);
return titleView;
}
This doesn't work. The title appears above overlapped with a bunch of other titles, then the moment I start scrolling (horizontally), they jump back into the wrong place, horizontally between the images rather than above.
PS> Please do not suggest anything that has to do with NIB or XIB placement. I am using a UICollectionView, NOT a UICollectionViewController, so I actually have no prototypical cell to work with. The layout is being done entirely programatically -- from code alone -- so I can't simply open a XIB file and adjust the location of a text box.
Amending the attributes returned by -layoutAttributesForElementsInRect is the right approach, but if you want to alter the position of offscreen headers and footers, you may need to fetch the supplementary view attributes yourself.
For example, in your UICollectionViewFlowLayout subclass:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *attributesArray = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
// the call to super only returns attributes for headers that are in the bounds,
// so locate attributes for out of bounds headers and include them in the array
NSMutableIndexSet *omittedSections = [NSMutableIndexSet indexSet];
for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
if (attributes.representedElementCategory == UICollectionElementCategoryCell) {
[omittedSections addIndex:attributes.indexPath.section];
}
}
for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
[omittedSections removeIndex:attributes.indexPath.section];
}
}
[omittedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
atIndexPath:indexPath];
[attributesArray addObject:attributes];
}];
for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
// adjust any aspect of each header's attributes here, including frame or zIndex
}
}
return attributesArray;
}
CollectionView Header height is set below Collectionview delegate
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
And Set view in Collectionview Header in Below Delegate
- (UICollectionReusableView*)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
UICollectionReusableView * view = nil;
if ([kind isEqualToString:UICollectionElementKindSectionHeader])
{
ColorSectionHeaderView *header = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader
withReuseIdentifier:NSStringFromClass([ColorSectionHeaderView class])
forIndexPath:indexPath];
header.sectionIndex = indexPath.section;
header.hideDelete = collectionView.numberOfSections == 1; // hide when only one section
header.delegate = self;
view = header;
}
return view;
}
Ragistred Class in ViewDidLoad
-(void)ViewDidLoad
{
[collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([ColorSectionFooterView class]) bundle:nil]
forSupplementaryViewOfKind:UICollectionElementKindSectionFooter
withReuseIdentifier:NSStringFromClass([ColorSectionFooterView class])];
[Super ViewDidLoad];
}
I have a basic grid in a UICollectionView. It's a simple 2 column, multiple row layout using the UICollectionViewDelegateFlowLayout. When a cell is selected, I want to dim the background, float the cell to the center of the screen and then have a workflow based on the selected cell. I'm fairly new to UICollectionViews, and I'm not sure of the best way to go about this.
Should I have a custom layout of the UICollectionView for when a cell is selected?
Or is there a way I can animate the selected cell without having to create a new layout
If anyone can just get me going on the right direction, I think I'll be good to research how to execute it.
After trying several (and rather hacky) solutions, I solved this by writing my own UICollectionView layout. Previous solutions I attempted:
Add a custom UIView overtop the UICollectionView and attempt to fake the original cell moving by overlaying it, dimming the background, and animating the new UIView
Animate the cell without creating a brand new layout
I was trying to avoid it, but after I got into coding it, it was a lot less painful than I originally thought.
Here is my code, for anyone interested:
.h:
#interface FMStackedLayout : UICollectionViewLayout
#property(nonatomic,assign) CGPoint center;
#property(nonatomic,assign) NSInteger cellCount;
-(id)initWithSelectedCellIndexPath:(NSIndexPath *)indexPath;
#end
.m:
#define ITEM_WIDTH 128.0f
#define ITEM_HEIGHT 180.0f
static NSUInteger const RotationCount = 32;
static NSUInteger const RotationStride = 3;
static NSUInteger const PhotoCellBaseZIndex = 100;
#interface FMStackedLayout ()
#property(strong,nonatomic) NSArray *rotations;
#property(strong,nonatomic) NSIndexPath *selectedIndexPath;
#end
#implementation FMStackedLayout
#pragma mark - Lifecycle
-(id)initWithSelectedCellIndexPath:(NSIndexPath *)indexPath{
self = [super init];
if (self) {
self.selectedIndexPath = indexPath;
[self setup];
}
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder{
self = [super init];
if (self){
[self setup];
}
return self;
}
-(void)setup{
NSMutableArray *rotations = [NSMutableArray arrayWithCapacity:RotationCount];
CGFloat percentage = 0.0f;
for (NSUInteger i = 0; i < RotationCount; i++) {
// Ensure that each angle is different enough to be seen
CGFloat newPercentage = 0.0f;
do {
newPercentage = ((CGFloat)(arc4random() % 220) - 110) * 0.0001f;
} while (fabsf(percentage - newPercentage) < 0.006);
percentage = newPercentage;
CGFloat angle = 2 * M_PI * (1.0f + percentage);
CATransform3D transform = CATransform3DMakeRotation(angle, 0.0f, 0.0f, 1.0f);
[rotations addObject:[NSValue valueWithCATransform3D:transform]];
}
self.rotations = rotations;
}
#pragma mark - Layout
-(void)prepareLayout{
[super prepareLayout];
CGSize size = self.collectionView.frame.size;
self.cellCount = [self.collectionView numberOfItemsInSection:0];
self.center = CGPointMake(size.width / 2.0, size.height / 2.0);
}
-(CGSize)collectionViewContentSize{
return self.collectionView.frame.size;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.size = CGSizeMake(ITEM_WIDTH, ITEM_HEIGHT);
attributes.center = self.center;
if (indexPath.item == self.selectedIndexPath.item) {
attributes.zIndex = 100;
}else{
attributes.transform3D = [self transformForPersonViewAtIndex:indexPath];
}
return attributes;
}
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
NSMutableArray *attributes = [NSMutableArray array];
for (NSInteger i = 0; i < self.cellCount; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i
inSection:0];
[attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
return attributes;
}
#pragma mark - Private
-(CATransform3D)transformForPersonViewAtIndex:(NSIndexPath *)indexPath{
NSInteger offset = (indexPath.section * RotationStride + indexPath.item);
return [self.rotations[offset % RotationCount] CATransform3DValue];
}
#end
Then, on your UICollectionView, you call
MyLayout *stackedLayout = [[MyLayout alloc] initWithSelectedCellIndexPath:indexPath];
[stackedLayout invalidateLayout];
[self.collectionView setCollectionViewLayout:stackedLayout
animated:YES];
Animations between cells will be handled by the custom layout.
Seems to be a multi-part question, so I'll answer part of it.
UICollectionviewCells do not automatically adjust on highlight or selection. It will only tell you when the cell is highlighted or selected, with one exception:
For the most part, the collection view modifies only the properties of
a cell to indicate that it is selected or highlighted; it does not
change the visual appearance of your cells, with one exception. If a
cell’s selectedBackgroundView property contains a valid view, the
collection view shows that view when the cell is highlighted or
selected.
Otherwise, you must do the visual highlighting manually. Usually by adjusting either the .alpha property of the entire cell, or swapping out the .image property of a background UIImageView in the cell itself using one or more of the following methods, which are accessible in the <UICollectionViewDelegate> Protocol:
collectionView:didDeselectItemAtIndexPath:
collectionView:didHighlightItemAtIndexPath:
collectionView:didSelectItemAtIndexPath:
collectionView:didUnhighlightItemAtIndexPath:
collectionView:shouldDeselectItemAtIndexPath:
collectionView:shouldHighlightItemAtIndexPath:
collectionView:shouldSelectItemAtIndexPath:
As for the rest of your question,I wish I could be of more help, but I'm not as fluent at the custom view animation.