Ios 如何开发自定义UICollectionViewLayout,该布局具有交错的列和自调整大小的单元格?

Ios 如何开发自定义UICollectionViewLayout,该布局具有交错的列和自调整大小的单元格?,ios,swift,Ios,Swift,我正在开发我已经在Android上开发的应用程序的iOS版本。此应用程序具有以下2列自调整大小(固定宽度但可变高度)单元格网格: 在Android版本中实现这一点很容易,因为Google为其RecyclerView提供了一个StaggedGridLayoutManager。指定列数和滚动方向,即可完成 默认的UICollectionViewlayoutUICollectionViewFlowLayout不允许我正在寻找的交错布局,因此我必须实现自定义布局。我看了两个WWDC视频,讨论了这个主题

我正在开发我已经在Android上开发的应用程序的iOS版本。此应用程序具有以下2列自调整大小(固定宽度但可变高度)单元格网格:

在Android版本中实现这一点很容易,因为Google为其
RecyclerView
提供了一个
StaggedGridLayoutManager
。指定列数和滚动方向,即可完成

默认的
UICollectionView
layout
UICollectionViewFlowLayout
不允许我正在寻找的交错布局,因此我必须实现自定义布局。我看了两个WWDC视频,讨论了这个主题(和),我或多或少对如何实现它有了想法

步骤1。首先计算布局的近似值

第2步。然后使用autolayout创建单元格并调整大小

第3步。然后,控制器将单元格大小通知用户,以便更新布局

当我试图编写这些步骤时,我产生了怀疑。我找到了一个解释如何创建具有交错列的自定义布局的方法,但它不使用autolayout来获取单元格的大小。这给我留下了以下问题:

在步骤2中,如何以及何时获得单元格大小?


在步骤3中,我如何以及何时才能通知布局更改?我意识到这不是一个完整的答案,但是关于步骤2和步骤3的一些提示可以在
UICollectionViewLayout
的子类化注释中找到

我假设您已经对
UICollectionViewFlowLayout
进行了子类化,因为我相信这是调整布局以获得所需交错外观的一个良好起点

对于步骤2
layouttributesforementsinrect(:)
应提供自大小单元格的布局属性


对于步骤3,您的布局将使用更改的单元格大小进行调用。

我想指出,正如您所提到的,这正是帮助您实现此布局的教程

回答您的问题-关于本教程:

在步骤2中,如何以及何时获得单元格大小?

为了获得单元格高度,实现了一个委托方法,该方法在自定义
UICollectionViewLayout
prepareLayout
方法中调用。这个方法被称为一次(或者两次,我只是试图用
print
语句运行它,我得到了两次调用)。
prepareLayout
的要点是初始化单元格的
frame
属性,换句话说,提供每个单元格的确切大小。我们知道宽度是恒定的,只有
高度
在变化,因此在
prepareLayout
的这一行中:

let cellHeight = delegate.collectionView(collectionView!,
      heightForItemAtIndexPath: indexPath, withWidth: width)
我们通过在
UICollectionViewController
中实现的委托方法获得单元格的高度。这适用于我们希望在
collectionView
中显示的所有单元格。在获得并修改每个单元的高度后,我们缓存结果,以便稍后检查

之后,
collectionView
要获得屏幕上每个单元格的大小,只需查询缓存中的信息。这是在自定义
UICollectionViewLayout
类的
layouttributesforementsinrect
方法中完成的

此方法由
UICollectionViewController
自动调用。当
UICollectionViewController
需要屏幕上单元格的布局信息时(例如,由于滚动或第一次加载),您可以从缓存中返回在
prepareLayout
中填充的属性

作为您问题的结论:在步骤2中,如何以及何时获得单元格大小?

回答:每个单元格大小都是在自定义
UICollectionViewFlowLayout
prepareLayout
方法中获得的,并在
UICollectionView
的生命周期的早期进行计算

在步骤3中,如何以及何时通知布局更改?

请注意,本教程不考虑在运行时添加的新单元格:

注意:由于每当集合视图的布局无效时都会调用prepareLayout(),因此在典型的实现中,在许多情况下可能需要在此处重新计算属性。例如,UICollectionView的边界可能会更改(例如方向更改时),或者项目可能会从集合中添加或删除。这些情况超出了本教程的范围,但在一个非常重要的实现中注意它们是很重要的

正如他所写的,这是一个您可能需要的非常重要的实现。然而,如果您的数据集很小(或出于测试目的),您可能会采用一种微不足道的(非常低效的)实现。当由于屏幕旋转或添加/删除单元格而需要使布局无效时,可以清除自定义
UICollectionViewFlowLayout
中的缓存,以强制
prepareLayout
重新初始化布局属性

例如,当您必须在collectionView上调用
reloadData
时,还要调用自定义布局类来删除缓存:

cache.removeAll()
“单元格大小”由布局子类中的UICollectionViewLayoutAttribute定义,这意味着您可以在每次有机会触摸它们时对其进行修改。您可以根据需要设置每个属性的大小

例如,您可以在LayoutAttributesOfelementsRect(:)中执行此操作,计算正确的大小并配置所有属性,然后将它们传递给collectionView。你也可以
- (void)prepareLayout {
    [_layoutMap removeAllObjects];
    _totalItemsInSection = [self.collectionView numberOfItemsInSection:0];
    _columnsYoffset = [self initialDataForColumnsOffsetY];

    if (_totalItemsInSection > 0 && self.totalColumns > 0) {
        [self calculateItemsSize];

        NSInteger itemIndex = 0;
        CGFloat contentSizeHeight = 0;

        while (itemIndex < _totalItemsInSection) {
            NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:itemIndex inSection:0];
            NSInteger columnIndex = [self columnIndexForItemAtIndexPath:targetIndexPath];

            // you need to implement this method and perform your calculations
            CGRect attributeRect = [self calculateItemFrameAtIndexPath:targetIndexPath]; 
            UICollectionViewLayoutAttributes *targetLayoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:targetIndexPath];
            targetLayoutAttributes.frame = attributeRect;

            contentSizeHeight = MAX(CGRectGetMaxY(attributeRect), contentSizeHeight);
            _columnsYoffset[columnIndex] = @(CGRectGetMaxY(attributeRect) + self.interItemsSpacing);
            _layoutMap[targetIndexPath] = targetLayoutAttributes;

            itemIndex += 1;
        }

        _contentSize = CGSizeMake(self.collectionView.bounds.size.width - self.contentInsets.left - self.contentInsets.right,
                                  contentSizeHeight);
    }
}
- (NSArray <UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSMutableArray<UICollectionViewLayoutAttributes *> *layoutAttributesArray = [NSMutableArray new];

    for (UICollectionViewLayoutAttributes *layoutAttributes in _layoutMap.allValues) {
        if (CGRectIntersectsRect(layoutAttributes.frame, rect)) {
            [layoutAttributesArray addObject:layoutAttributes];
        }
    }

    return layoutAttributesArray;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    return _layoutMap[indexPath];
}