I'm tasked with modifying one of our horizontal collectionViews so that it supports 2 possible cell widths based on the value of one of the properties of the item at that indexPath. This is rather simple to implement using UICollectionViewFlowLayout's handy delegate method sizeForItemAt(indexPath: ).
However, since our current collectionView uses UICompositionalLayout, I've been researching if it's possible to achieve the same behaviour using the compositional approach.
What I have tried so far:
Having 2 different cell classes and setting the width in the cells themselves, then setting the item and group sizes to an estimated with at exactly the midpoint of both widths (large cell width is 145, small cell width is 95, estimated width is 120). Result: this only resulted in every cell having a 120 width
Creating 2 different compositionalLayout items (small item, large item). Putting each in its own group and setting the fractional width to 1 Result: this also did not allow cell width to adjust on the fly.
Here is a screenShot of the desired result that I achieved using FlowLaout
I know UICompositionalLayout is amazing for handling complex layouts like Apples PhotoAlbum. But is one of its limitations not having an equivalent to sizeForItemAt(indexPath:)?
Here is how our diffableDataSource is set up (not currently supporting multiple cell widths):
private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<Int, UploadParameter>(
collectionView: collectionView) { [unowned self] collectionView, indexPath, media in
guard !media.isEmptyAudio else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: String(describing: AudioCell.self),
for: indexPath
) as! AudioCell
cell.duration = media.duration
return cell
}
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: String(describing: MediaCell.self),
for: indexPath
) as! MediaCell
cell.videoIconImageView.isHidden = !media.isVideo
return configuredCellInTimeline(
collectionView,
for: cell,
at: indexPath,
with: media
)
}
var snapshot = NSDiffableDataSourceSnapshot<Int, UploadParameter>()
snapshot.appendSections([0])
dataSource.apply(snapshot, animatingDifferences: false)
}
Furthermore, here is the configureCellInTimeLine method that we return when configuring the diffableDataSource (not currently supporting multiple cell widths):
private func configuredCellInTimeline(
_ collectionView: UICollectionView,
for cell: MediaCell,
at indexPath: IndexPath,
with media: UploadParameter) -> MediaCell {
let idx = (mediaItems.count - 1) - indexPath.item
let idxPath = IndexPath(row: idx, section: 0)
cell.duration = media.duration
cell.deletion = deletion
cell.selection = selection
cell.color = media.color
cell.isVideo = media.isVideo
cell.isFetchedAsset = false
// Remove any video cells from the selected index paths
if cell.isVideo,
selectedMediaIndexPaths.contains(idxPath) {
selectedMediaIndexPaths.remove(idxPath)
}
switch media.entity {
case let .image(image):
cell.image = image
case let .video(_, image):
cell.image = image
case let .asset(asset):
cell.isFetchedAsset = true
MediaService.shared.fetchImage(
forAsset: asset,
resizedTo: ImageSize.thumbnail) { image in
guard cell.isFetchedAsset else { return }
cell.image = image
}
case .empty:
if media.isEmptyAudio {
cell.duration = media.duration
return cell
} else {
cell.image = nil
}
}
if self.selectedMediaIndexPaths.isEmpty {
cell.selectionType = .normal
} else if selectedMediaIndexPaths.contains(idxPath) {
cell.selectionType = .selected
} else {
cell.selectionType = .unselected
}
cell.onImageSelectionChange = { [weak self] didSelect in
DispatchQueue.main.async {
self?.timelineCell(at: indexPath, onSelectionChange: didSelect)
}
}
cell.onDurationTap = { [weak self] in
DispatchQueue.main.async {
self?.tappedTimelineCellDuration(at: indexPath)
}
}
cell.onTap = { [weak self] in
DispatchQueue.main.async {
self?.tappedTimelineCell(at: indexPath)
}
}
cell.onLongPress = { [weak self] in
DispatchQueue.main.async {
self?.onLongPress.send()
}
}
cell.onDelete = { [weak self] in
DispatchQueue.main.async {
self?.deletedTimelineCell(at: indexPath)
}
}
cell.layoutIfNeeded()
return cell
}
finally, here is how I achieved the desired result of supporting multiple cell widths using a flowLayout and sizeForItemAt(indexPath:):
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let largeCellWidth = 145
let standardCellWidth = 95
let cellHeight = 170
let item = mediaItems.reversed()[indexPath.item]
return CGSize(width: item.isLongDuration ? largeCellWidth : standardCellWidth, height: cellHeight)
}
hope this wasn't too long Looking forward to hearing what you guys and gals