Dynamic layout and content adaptive size of IOS uicollectionview

Time:2021-11-26

In daily development, it often involves the display of some condition buttons and content labels. There are many attributes that need to be added. It is obviously too cumbersome and easy to implement with buttons

Dynamic layout and content adaptive size of IOS uicollectionview

image.png

And if these labels need to be set dynamically, it will become more complex.
This article implements these requirements through uicollectionview. Because the display content is dynamically controlled by the server, the first thing to do here is to make the cell size of the collectionview adaptive to the width of the text. Then dynamically set the size of the collectionview.

Calculate the cell size

Because the codes for calculating text width are general, they are directly insizeForItemAtIndexPathMethod returns the width and height of the cell.

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
    NSDictionary *attribute = @{NSFontAttributeName: self.textFont};
    CGSize currentLabelSize = [self.dataAry[indexPath.item] sizeWithAttributes:attribute];
    
    return CGSizeMake(ceil(currentLabelSize.width) + self.leftAndRightSpacing*2, self.itemCellHeight);
}

However, after setting, you may find that the spacing of items is not fixed, as shown in the following figure:

Dynamic layout and content adaptive size of IOS uicollectionview

image.png

Override uicollectionviewflowlayout method

To arrange the collectionview neatly, you need to use flowlayout. Here, you can re layout the collectionview by inheriting uicollectionviewflowlayout. Uicollectionviewflowlayout is a layout class, which inherits from uicollectionviewlayout and mainly involves the following two methods:

- (CGSize)collectionViewContentSize; 

- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;

The collectionviewcontentsize method is used to return the size of the collectionview content, not the frame size of uicollectionview.
Layoutattributes forelementsinrect, here is the array that returns all layout attributes.
By rewriting these two methods, you can re layout the content, so that the arrangement of each cell can be compact.

First, a custom class inherits fromUICollectionViewFlowLayout, define the required properties

@interface LiveBroadcastFlowLayout : UICollectionViewFlowLayout

///Array contents
@property (nonatomic, strong) NSArray *dataArry;
///Item height
@property (nonatomic, assign) CGFloat itemHeight;
///Item left and right offset
@property (nonatomic, assign) CGFloat itemWidthSpacing;
///Display font
@property (nonatomic, strong) UIFont *textFont;
///Uicollectionview width
@property (nonatomic, assign) CGFloat contentWidth;

@end

Through code comments, you can understand the method and function of rewriting.
Calculate the size of contentsize:

- (CGSize)collectionViewContentSize{
    CGSize size = CGSizeZero;
    NSInteger itemCount = 0;
//Gets the number of items in the collectionview. If it is 0, cgsizezero is returned
    if ([self.collectionView.dataSource respondsToSelector:@selector(collectionView:numberOfItemsInSection:)]) {
        itemCount = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:0];
    }
    if (CGSizeEqualToSize(size, CGSizeZero) && itemCount == 0) {
        return CGSizeZero;
    }
//Content width
    NSInteger lineWidth = 0;
//Number of display lines
    NSUInteger rowCount = 1;
    for (int i = 0; i < itemCount; ++i) {
//Self.dataarray is a content array
//Calculate the item width based on the passed in font size self.textfont
//Then calculate with the width self.contentwidth of the incoming collectionview
        NSDictionary *attribute = @{NSFontAttributeName: self.textFont};
        CGSize currentLabelSize = [self.dataArry[i] sizeWithAttributes:attribute];
//Self.itemwidethspacing is the reserved width for display, which can be set according to requirements
        CGFloat cellWidth = currentLabelSize.width + self.itemWidthSpacing*2;
        if (i == (itemCount - 1)) {
            lineWidth = lineWidth + cellWidth;
        } else {
            lineWidth = lineWidth + self.minimumInteritemSpacing + cellWidth;
        }
//Calculate the number of items displayed in a row
        if (lineWidth > (NSInteger)self.contentWidth) {
            rowCount++;
            lineWidth = cellWidth + self.minimumInteritemSpacing + cellWidth;
        }
    }
//Finally, the height required for the content display of collectionview is calculated
    size.width = self.contentWidth;
    size.height = rowCount * self.itemHeight + (rowCount - 1) * self.minimumLineSpacing  + self.sectionInset.top + self.sectionInset.bottom;
    
    return size;
}

Control the display of the internal item layout of collectionview:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSMutableArray* attributes = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
//Fix the first item on the top left to prevent confusion when only one item is displayed in a row
    if (attributes.count > 0) {
        UICollectionViewLayoutAttributes *currentLayoutAttributes = attributes[0];
        CGRect frame = currentLayoutAttributes.frame;
        frame.origin.x = 0;
        currentLayoutAttributes.frame = frame;
    }
    
    for(int i = 1; i < [attributes count]; ++i) {
        NSDictionary *attribute = @{NSFontAttributeName: self.textFont};
        CGSize labelSize = [self.dataArry[i] sizeWithAttributes:attribute];
        UICollectionViewLayoutAttributes *currentLayoutAttributes = attributes[i];
        UICollectionViewLayoutAttributes *prevLayoutAttributes = attributes[i - 1];
        CGFloat cellWidth = ceil(labelSize.width) + self.itemWidthSpacing*2;
        currentLayoutAttributes.size = CGSizeMake(cellWidth, self.itemHeight);
        NSInteger origin = CGRectGetMaxX(prevLayoutAttributes.frame);
//If the width of the current item + the maximum width of the previous item + the item spacing < = the width of collectionview, one line can be accommodated. Modify the x-axis position of the current item, otherwise it will be displayed on the left
        if (origin + self.minimumInteritemSpacing + currentLayoutAttributes.frame.size.width < self.contentWidth) {
            CGRect frame = currentLayoutAttributes.frame;
            frame.origin.x = origin + self.minimumInteritemSpacing;
            currentLayoutAttributes.frame = frame;
        } else {
            CGRect frame = currentLayoutAttributes.frame;
            frame.origin.x = 0;
            currentLayoutAttributes.frame = frame;
        }
    }

    return attributes;
}

In the layoutattributesforelementsinrect method, starting from the second item, the position of each cell is the position of the previous cell + maximumspacing. If it does not exceed the maximum width of this line, the starting position and size (i.e. frame) of the current cell will be changed. What does it mean not to change if it exceeds? It is to keep the original position. The original position here may be different from what we think. It is not the fixed position at the beginning, but the adjusted position, because changing the previous cell here has a good impact on the subsequent cells. It should be that the y-axis position will be adjusted automatically.

It’s not over yet

In many cases, uicollectionview is not used alone. It is more nested in the cell of uitableview and is responsible for displaying a small part of the content.

So how to dynamically adjust the size and height of uicollectionview in uitableviewcell?

first

Add uicollectionview in uitableviewcell and set constraints:

Dynamic layout and content adaptive size of IOS uicollectionview

image.png

In addition to the coordinate position, focus on setting the width and height constraints of uicollectionview, and set any value here (close to the real size).

then

The width and height constraints are associated through Xib

@interface MyLiveBroadcastCollectView () <UICollectionViewDataSource, UICollectionViewDelegate>

@property (weak, nonatomic) IBOutlet LiveBroadcastFlowLayout *flowLayout;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *pictureCollectionViewHeightCons;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *pictureCollectionViewWidthCons;

@property (nonatomic, strong) NSArray *dataAry;

@end

Calling methods externallyem_displayWithID:

- (void)em_displayWithID:(id)model{
    self.dataAry = model;
    self.flowLayout.dataArry = model;
    [self reloadData];
    
    //[tableview reload] is an asynchronous operation, and [uicollectionview reload data] does not actually load all items
    //Therefore, collectionviewlayout.collectionviewcontentsize is incorrect
    //So you need to override the collectionviewlayout
    CGFloat updateHeight = self.collectionViewLayout.collectionViewContentSize.height;// Here, take the newly opened contentsize to update the height constraint.
    
    self.pictureCollectionViewHeightCons.constant = updateHeight;
    self.pictureCollectionViewWidthCons.constant = self.contentWidth;
}

This way backupdateHeightIs calculated by rewriting the method, andself.contentWidthThen pass[UIScreen mainScreen].bounds.size.widthCalculate to fix becauseUITableViewCelladoptlayoutIfNeededOnly afterUICollectionViewSo it is only applicable to the case where the uicollectionview width is fixed for the time being.

next

Complete the method call by assigning a value to uitableviewcell

- (CGFloat)calculateRowHeightWithId:(id)model{
    self.dataModel = model;
    
    if (self.dataModel.height != 0) {
        return self.dataModel.height;
    }
    
    [self em_displayWithID:self.dataModel];
    
    [self layoutIfNeeded];
    
    self.dataModel.height = CGRectGetMaxY(self.stackView.frame);
    return CGRectGetMaxY(self.stackView.frame);
}

- (void)em_displayWithID:(id)model{
    self.dataModel = model;
    /*. omit the method*/
    [self.tagsCollectionView em_displayWithID:self.dataModel.tagArry];
}

By calling the calculation height method of uitableviewcellcalculateRowHeightWithId, in the of uitableviewcellem_displayWithID:Method to call the assignment method of uicollectionview.

Finally, attach the appearance of the renderings:

Dynamic layout and content adaptive size of IOS uicollectionview

image.png

Reference articles and source code:
Adaptive size of collectionview under AutoLayout