UICollectionViewCompositionalLayout - Unexpected layout with fractionalWidth width and self sizing cells - ios

Requirement
I am trying to make a CollectionView section that has 1 large item then 4 half-width items below like this:
Each cell will contain labels that support multiline text or variable length with dynamic type support.
Approach
I have written the following compositional layout code which constructs 3 groups. A fullWidthGroup contains a single item that is full width (item 1). A halfWidthGroup will hold 2 items per "row" and a main outer group group which is made up of 1 fullWidthGroup and 2 halfWidthGroups.
private let compositionalLayout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, environment) -> NSCollectionLayoutSection? in
let margin: CGFloat = 8
// Items
let fullWidthItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))
let fullWidthItem = NSCollectionLayoutItem(layoutSize: fullWidthItemSize)
fullWidthItem.contentInsets = .init(top: 0, leading: margin, bottom: 0, trailing: margin)
let halfWidthItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(50))
let halfWidthItem = NSCollectionLayoutItem(layoutSize: halfWidthItemSize)
halfWidthItem.contentInsets = .init(top: 0, leading: margin, bottom: 0, trailing: margin)
// Groups
let fullWidthGroup = NSCollectionLayoutGroup.horizontal(layoutSize: fullWidthItemSize, subitems: [fullWidthItem])
let halfWidthGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: halfWidthItemSize.heightDimension)
let halfWidthGroup = NSCollectionLayoutGroup.horizontal(layoutSize: halfWidthGroupSize, subitems: [halfWidthItem])
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(150))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [fullWidthGroup, halfWidthGroup, halfWidthGroup])
group.interItemSpacing = .fixed(margin)
// Section
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = margin
return section
})
This gives the desired result for basic cells that do not contain any constraints (to self size them).
Problem
As soon as I create a cell that has constraints that can be used to correctly evaluate the size of the cell width of the cells is unexpectedly changed.
Resulting in the following layout.
Expectation
I would expect the last 4 items to take up half the width but instead, they appear to be taking up less space. Item 1 which should be full width with an 8 point margin on each side seems to also be sized incorrectly with ~16 point margin on the trailing edge.
Full Code
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let compositionalLayout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, environment) -> NSCollectionLayoutSection? in
let margin: CGFloat = 8
// Items
let fullWidthItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))
let fullWidthItem = NSCollectionLayoutItem(layoutSize: fullWidthItemSize)
fullWidthItem.contentInsets = .init(top: 0, leading: margin, bottom: 0, trailing: margin)
let halfWidthItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(50))
let halfWidthItem = NSCollectionLayoutItem(layoutSize: halfWidthItemSize)
halfWidthItem.contentInsets = .init(top: 0, leading: margin, bottom: 0, trailing: margin)
// Groups
let fullWidthGroup = NSCollectionLayoutGroup.horizontal(layoutSize: fullWidthItemSize, subitems: [fullWidthItem])
let halfWidthGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: halfWidthItemSize.heightDimension)
let halfWidthGroup = NSCollectionLayoutGroup.horizontal(layoutSize: halfWidthGroupSize, subitems: [halfWidthItem])
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(150))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [fullWidthGroup, halfWidthGroup, halfWidthGroup])
group.interItemSpacing = .fixed(margin)
// Section
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = margin
return section
})
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(UINib(nibName: "BasicCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
collectionView.collectionViewLayout = compositionalLayout
collectionView.dataSource = self
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.contentView.backgroundColor = .red
return cell
}
}
The BasicCollectionViewCell is created in a nib and contains a stack view (added to the contentView). The StackView is pinned to all edges with a priority of 1000. Inside the stackview is a label.

Related

Horizontal UICollectionViewCompositionalLayout with next cell hint

I'm experimenting with UICollectionViewCompositionalLayout and trying to achieve a similar effect to the Apple Music app, especially to the "hint" indicating there are more cells to scroll:
So far, I have managed to make a similar grid, but I can't make the "visual hint" appear among all screen widths.
This is what I tried:
func createBasicListLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// we will be working with this part
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalHeight(1.0), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.edgeSpacing = .init(leading: .fixed(10), top: nil, trailing: .fixed(10), bottom: nil)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.interGroupSpacing = 0
section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20)
let layout = UICollectionViewCompositionalLayout(section: section)
layout.configuration.scrollDirection = .horizontal
return layout
}
Which results in:
How can I make the layout show a hint on a third cell? Thanks in advance!

Dynamic width for center UICollectionView's item

I'm working on calendar in which I need to highlight and expand the central cell. It should looks like this:
First I tried to use a compositionalLayout with NSCollectionLayoutItem->widthDimension: .estimated(24).
Collection rendered correctly, but behaved glitchy when scrolling: wrong interim spacing.
UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
guard let sectionKind = TodaySection(rawValue: sectionIndex) else { return nil }
let layoutItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
widthDimension: .estimated(24),
heightDimension: .fractionalHeight(1.0)))
layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
let groupSize: NSCollectionLayoutSize!
switch sectionKind {
case .main:
groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
}
let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [layoutItem])
layoutGroup.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0)
layoutGroup.interItemSpacing = NSCollectionLayoutSpacing.fixed(12)
let section = NSCollectionLayoutSection(group: layoutGroup)
section.orthogonalScrollingBehavior = .continuous
return section
}
when the cell becomes the center, then I change dots width constraint with animation and call collectionViewLayout.invalidateLayout() at the end. And here is the result:
I can't find a way to dynamically change the cell width for a CompositionalLayout. Or maybe I should stop using compositionalLayout for this issue and use flowLayout instead?

UICollectionViewCell with three UICollectionViews using Compositional Layout

I'm trying to accomplish a dynamic cell with the new collection view compositional layout
basically this is what I have:
UICollectionView (Compositional Layout)
UICollectionViewCell
innerStackView (Vertical UIStack ) // Contains postDescription and 'from Riyadh' Label
....
UICollectionView (Compositional Layout)
UICollectionView (Flow Layout -Horizontal)
UICollectionView(Compositional Layout)
I have tried two approaches:
1- Adding the inner collection views inside a vertical stack,
2- Adding the inner collection views inside the container view with constraints
This is what I have currently:
The top UICollectionView (Compositional Layout)
func setupCollectionViewLayout() -> UICollectionViewLayout {
let size = NSCollectionLayoutSize(
widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
heightDimension: NSCollectionLayoutDimension.estimated(300)
)
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .zero
section.interGroupSpacing = 0
let headerFooterSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.3)
)
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top
)
section.boundarySupplementaryItems = [sectionHeader]
return UICollectionViewCompositionalLayout(section: section)
}
The Cell (Using Container View Constraints)
class FeedsCell: UICollectionViewCell {
#IBOutlet weak var containerView: UIView!
#IBOutlet weak var innerStackView: UIStackView!
#IBOutlet weak var postDescription: UILabel!
var imageGalleryCollectionView: UICollectionView!
var reactionCollectionView: UICollectionView!
var commentCollectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
}
func with(viewModel: FeedsCellViewModel) -> Self {
postDescription.text = ".random(in: 0...255)"
if !viewModel.imageProvider.dataSource.isEmpty {
let imageLayout = setupImageLayout()
imageGalleryCollectionView = UICollectionView(frame: .zero, collectionViewLayout: imageLayout)
imageGalleryCollectionView.delegate = viewModel.imageProvider
imageGalleryCollectionView.dataSource = viewModel.imageProvider
containerView.addSubview(imageGalleryCollectionView)
}
if !viewModel.reactionProvider.dataSource.isEmpty {
let reactionLayout = UICollectionViewFlowLayout()
reactionLayout.scrollDirection = .horizontal
reactionCollectionView = UICollectionView(frame: .zero, collectionViewLayout: reactionLayout)
reactionCollectionView.showsHorizontalScrollIndicator = false
reactionCollectionView.dataSource = viewModel.reactionProvider
reactionCollectionView.delegate = viewModel.reactionProvider
innerStackView.addArrangedSubview(reactionCollectionView)
reactionCollectionView.anchor(heightConstant: 32)
}
if !viewModel.commentProvider.dataSource.isEmpty {
let commentLayout = setupCommentLayout()
commentCollectionView = UICollectionView(frame: .zero, collectionViewLayout: commentLayout)
commentCollectionView.dataSource = viewModel.commentProvider
commentCollectionView.delegate = viewModel.commentProvider
containerView.addSubview(commentCollectionView)
imageGalleryCollectionView.anchor(top: innerStackView.bottomAnchor, left: innerStackView.leftAnchor, bottom: commentCollectionView.topAnchor, right: innerStackView.rightAnchor)
commentCollectionView.anchor(top: imageGalleryCollectionView.bottomAnchor, left: innerStackView.leftAnchor, bottom: containerView.bottomAnchor, right: innerStackView.rightAnchor)
}
return self
}
override func prepareForReuse() {
super.prepareForReuse()
imageGalleryCollectionView?.removeFromSuperview()
reactionCollectionView?.removeFromSuperview()
commentCollectionView?.removeFromSuperview()
}
func setupCommentLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout.list(using: .init(appearance: .insetGrouped))
return layout
}
func setupImageLayout() -> UICollectionViewCompositionalLayout {
let mainItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(2/3),
heightDimension: .fractionalHeight(1.0)))
mainItem.contentInsets = NSDirectionalEdgeInsets(
top: 2,
leading: 0,
bottom: 0,
trailing: 2)
let pairItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5)))
pairItem.contentInsets = NSDirectionalEdgeInsets(
top: 2,
leading: 2,
bottom: 0,
trailing: 2)
let trailingGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1/3),
heightDimension: .fractionalHeight(1.0)),
subitem: pairItem,
count: 2)
let mainWithPairGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(4/9)),
subitems: [mainItem, trailingGroup])
let nestedGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(1.0)),
subitems: [
mainWithPairGroup,
]
)
let section = NSCollectionLayoutSection(group: nestedGroup)
return UICollectionViewCompositionalLayout(section: section)
}
}
The Result
The Cell (using UIStackView)
func with(viewModel: FeedsCellViewModel) -> Self {
postDescription.text = ".random(in: 0...255)"
if !viewModel.imageProvider.dataSource.isEmpty {
let imageLayout = setupImageLayout()
imageGalleryCollectionView = UICollectionView(frame: .zero, collectionViewLayout: imageLayout)
imageGalleryCollectionView.delegate = viewModel.imageProvider
imageGalleryCollectionView.dataSource = viewModel.imageProvider
innerStackView.addArrangedSubview(imageGalleryCollectionView)
}
if !viewModel.reactionProvider.dataSource.isEmpty {
let reactionLayout = UICollectionViewFlowLayout()
reactionLayout.scrollDirection = .horizontal
reactionCollectionView = UICollectionView(frame: .zero, collectionViewLayout: reactionLayout)
reactionCollectionView.showsHorizontalScrollIndicator = false
reactionCollectionView.dataSource = viewModel.reactionProvider
reactionCollectionView.delegate = viewModel.reactionProvider
innerStackView.addArrangedSubview(reactionCollectionView)
reactionCollectionView.anchor(heightConstant: 32)
}
if !viewModel.commentProvider.dataSource.isEmpty {
let commentLayout = setupCommentLayout()
commentCollectionView = UICollectionView(frame: .zero, collectionViewLayout: commentLayout)
commentCollectionView.dataSource = viewModel.commentProvider
commentCollectionView.delegate = viewModel.commentProvider
innerStackView.addArrangedSubview(commentCollectionView)
}
return self
}
The Result
It's really been a struggle, it's my 5th day trying to figure this out.

UICollectionViewCompositionalLayout: right align cells within a section

I have this layout, where the main picture is a UICollectionView, and the thumbnails are in their own UICollectionView.
The design asks for the thumbnails to be right-aligned, so the content in the UICollectionView needs to be pushed to the right. How can I do that? I only need to support iOS 13 and up, so am using UICollectionViewCompositionalLayout.
private func createThumbsLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalHeight(1), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
return UICollectionViewCompositionalLayout(section: section)
}
I found this:
thumbsCollectionView.semanticContentAttribute = .forceRightToLeft
Which works, if I then also reverse the images and reverse all IndexPath math to deal with selection and all that. It's not ideal so would love a better solution. Surely there must be some kind of alignment property? Same would be useful for a bottom-aligned vertical collectionview?

Horizontal UICollectionViewCompositionalLayout displaying section headers over item cells

I am attempting to use UICollectionViewCompositionalLayout to create this design:
The intention here is for the collection view to scroll horizontally, with the items also scrolling vertically when they overflow using NSCollectionLayoutSection's orthogonalScrollingBehavior property.
This is the layout I have right now:
private lazy var collectionViewLayout: UICollectionViewLayout = {
let sectionWidth: CGFloat = 256.0
let layoutItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50.0))
let layoutItem = NSCollectionLayoutItem(layoutSize: layoutItemSize)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(sectionWidth), heightDimension: .fractionalHeight(1.0))
let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: layoutGroupSize, subitems: [layoutItem])
layoutGroup.interItemSpacing = .fixed(8.0)
let layoutSectionHeaderItemSize = NSCollectionLayoutSize(widthDimension: .absolute(sectionWidth), heightDimension: .absolute(150.0))
let layoutSectionHeaderItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSectionHeaderItemSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 24.0, leading: 24.0, bottom: 24.0, trailing: 24.0)
layoutSection.boundarySupplementaryItems = [layoutSectionHeaderItem]
layoutSection.interGroupSpacing = 8.0
let layoutConfiguration = UICollectionViewCompositionalLayoutConfiguration()
layoutConfiguration.scrollDirection = .horizontal
layoutConfiguration.interSectionSpacing = 16.0
return UICollectionViewCompositionalLayout(section: layoutSection, configuration: layoutConfiguration)
}()
The result is:
Any ideas as to where I'm going wrong?
3 months later and no direct solutions to this one (either here, nor via bug report from Apple). So to get the design I was after I ended up reconfiguring the view structure.
In short, to get vertically-scrolling columns with a pinned header within a horizontally-scrolling collection view I placed a table view in the collection view cell, with the header elements above it, all in a stack view.
I kept the UICollectionViewCompositionalLayout on the collection view, which has been simplified:
private lazy var collectionViewLayout: UICollectionViewLayout = {
let layoutItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let layoutItem = NSCollectionLayoutItem(layoutSize: layoutItemSize)
let fractionalWidth: CGFloat = (Device.current.isPad) ? 0.5 : 1.0
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(fractionalWidth), heightDimension: .fractionalHeight(1.0))
let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: layoutGroupSize, subitems: [layoutItem])
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
let layoutConfiguration = UICollectionViewCompositionalLayoutConfiguration()
layoutConfiguration.scrollDirection = .horizontal
return UICollectionViewCompositionalLayout(section: layoutSection, configuration: layoutConfiguration)
}()

Resources