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.
Related
I want a header to appear above each section in my compositional layout, but I can't find a way to make them appear above the content rather than overlapping it, here is my layout code:
func createLayout() -> UICollectionViewLayout {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = .horizontal
config.interSectionSpacing = 16
let layout = UICollectionViewCompositionalLayout(sectionProvider: {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let leadingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .estimated(50)))
let widthFraction: CGFloat = UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height ? 0.2 : 0.8
let containerGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(widthFraction),
heightDimension: .estimated(50)),
subitems: [leadingItem])
let section = NSCollectionLayoutSection(group: containerGroup)
section.boundarySupplementaryItems = [self.makeHeader()]
section.orthogonalScrollingBehavior = .continuous
section.interGroupSpacing = 16
return section
}, configuration: config)
return layout
}
func makeHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let widthFraction: CGFloat = UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height ? 0.2 : 0.8
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(widthFraction),
heightDimension: .estimated(50))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: size,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top)
return sectionHeader
}
and here is a screenshot of what is happening, I need the text that says "Column1" to appear above, not overlapping the column cells:
Here was my solution, but it only allows for fixed-height headers on my sections:
you need to set
section.supplementariesFollowContentInsets = false
and then offset the section:
section.contentInsets = NSDirectionalEdgeInsets(top: 66, leading: 8, bottom: 0, trailing: 8)
func createLayout() -> UICollectionViewLayout {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = .horizontal
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let leadingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .estimated(50)))
let widthFraction: CGFloat = UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height ? 0.2 : 0.8
let containerGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(widthFraction),
heightDimension: .estimated(50)),
subitems: [leadingItem])
let section = NSCollectionLayoutSection(group: containerGroup)
section.boundarySupplementaryItems = [self.makeHeader()]
section.supplementariesFollowContentInsets = false
section.orthogonalScrollingBehavior = .continuous
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 66, leading: 8, bottom: 0, trailing: 8)
return section
}, configuration: config)
return layout
}
func makeHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let widthFraction: CGFloat = UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height ? 0.2 : 0.8
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(widthFraction),
heightDimension: .absolute(50))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: size, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
return sectionHeader
}
Add your header as in boundarySupplementaryItems inside your layout declaration.
let layout = UICollectionViewCompositionalLayout(sectionProvider: {
…
let widthFraction: CGFloat = UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height ? 0.2 : 0.8
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(widthFraction),
heightDimension: .estimated(50))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: size, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [sectionHeader]
return section
}
That should work.
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.
I currently have a UICollectionView which I'm using compositional layouts and Diffable Data sources. I'm not doing anything crazy just loading 150 cells into it and defining a 4 column layout. I seem to be encountering some weird behaviour whenever I tap the collectionview to scroll the items it seems to be disappearing... Below is my file with the entire code that I'm using so that you can copy & paste this and see this weird behaviour. Does anyone know why this might happening?
import UIKit
class ViewController: UIViewController {
private lazy var myCollectionViewLayout = MyCollectionViewLayout()
override func viewDidLoad() {
super.viewDidLoad()
setup()
// Do any additional setup after loading the view.
}
}
private extension ViewController {
func setup() {
let collectionVw = UICollectionView(frame: .zero, collectionViewLayout: myCollectionViewLayout.createLayout())
collectionVw.translatesAutoresizingMaskIntoConstraints = false
collectionVw.register(MyCustomCell.self, forCellWithReuseIdentifier: MyCustomCell.cellId)
self.view.addSubview(collectionVw)
NSLayoutConstraint.activate([
collectionVw.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
collectionVw.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
collectionVw.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
collectionVw.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
let dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionVw) { (collectionView, indexPath, item) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.cellId, for: indexPath) as? MyCustomCell
cell?.configure(at: indexPath.row)
return cell
}
collectionVw.dataSource = dataSource
var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()
snapshot.appendSections([0])
Range(0...150).forEach { item in
snapshot.appendItems([UUID()], toSection: 0)
}
dataSource.apply(snapshot, animatingDifferences: true)
}
}
class MyCollectionViewLayout {
func createLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnv: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalHeight(1))
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(0.25))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitem: item,
count: 4)
group.interItemSpacing = .fixed(16)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 0,
leading: 16,
bottom: 0,
trailing: 16)
return section
}
return layout
}
}
class MyCustomCell: UICollectionViewCell {
static let cellId = "MyCustomCell"
private var lbl: UILabel?
func configure(at index: Int) {
self.contentView.layer.cornerRadius = 8
self.contentView.backgroundColor = .blue
lbl = UILabel()
lbl?.translatesAutoresizingMaskIntoConstraints = false
lbl?.text = index.description
lbl?.textAlignment = .center
lbl?.font = .preferredFont(forTextStyle: .headline)
self.contentView.addSubview(lbl!)
NSLayoutConstraint.activate([
lbl!.topAnchor.constraint(equalTo: self.contentView.topAnchor),
lbl!.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
lbl!.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
lbl!.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor)
])
}
override func prepareForReuse() {
super.prepareForReuse()
lbl?.removeFromSuperview()
}
}
The reason is that your data source is a local variable inside your setup method (let dataSource...). Therefore it works just the one time to populate the collection view initially, and then vanishes in a puff of smoke, leaving the collection view with no data.
The data source needs to be a property of the view controller so that it persists! Once you make that change, all will be well.
It's actually a little tricky to do that, because the data source depends on the collection view. So you have to use lazy instantiation. You create the collection view with an empty layout and hook everything together in the first step of the setup.
Here's a minimal rewrite of your code; look for my comments explaining the changes.
import UIKit
class ViewController: UIViewController {
// delete; this is pointless
// private lazy var myCollectionViewLayout = MyCollectionViewLayout()
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
// need persistent pointer to collection view
let collectionVw = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())
// need persistent pointer to data source
lazy var dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionVw) { (collectionView, indexPath, item) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.cellId, for: indexPath) as? MyCustomCell
cell?.configure(at: indexPath.row)
return cell
}
}
private extension ViewController {
func setup() {
// let collectionVw = UICollectionView(frame: .zero, collectionViewLayout: myCollectionViewLayout.createLayout())
// everything is persistent, now just hook them together
collectionVw.collectionViewLayout = MyCollectionViewLayoutMaker.createLayout()
collectionVw.translatesAutoresizingMaskIntoConstraints = false
collectionVw.register(MyCustomCell.self, forCellWithReuseIdentifier: MyCustomCell.cellId)
self.view.addSubview(collectionVw)
NSLayoutConstraint.activate([
collectionVw.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
collectionVw.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
collectionVw.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
collectionVw.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
// let dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionVw) { (collectionView, indexPath, item) -> UICollectionViewCell? in
//
// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.cellId, for: indexPath) as? MyCustomCell
// cell?.configure(at: indexPath.row)
// return cell
// }
collectionVw.dataSource = dataSource
var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()
snapshot.appendSections([0])
Range(0...150).forEach { item in
snapshot.appendItems([UUID()], toSection: 0)
}
dataSource.apply(snapshot, animatingDifferences: true)
}
}
class MyCollectionViewLayoutMaker {
// make this static; the class is not the layout, it serves no purpose except as a factory
static func createLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnv: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalHeight(1))
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(0.25))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitem: item,
count: 4)
group.interItemSpacing = .fixed(16)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 0,
leading: 16,
bottom: 0,
trailing: 16)
return section
}
return layout
}
}
class MyCustomCell: UICollectionViewCell {
static let cellId = "MyCustomCell"
private var lbl: UILabel?
func configure(at index: Int) {
self.contentView.layer.cornerRadius = 8
self.contentView.backgroundColor = .blue
lbl = UILabel()
lbl?.translatesAutoresizingMaskIntoConstraints = false
lbl?.text = index.description
lbl?.textAlignment = .center
lbl?.font = .preferredFont(forTextStyle: .headline)
self.contentView.addSubview(lbl!)
NSLayoutConstraint.activate([
lbl!.topAnchor.constraint(equalTo: self.contentView.topAnchor),
lbl!.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
lbl!.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
lbl!.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor)
])
}
override func prepareForReuse() {
super.prepareForReuse()
lbl?.removeFromSuperview()
}
}
I am trying to achieve a 'tag cloud' effect which should look something like this -
Each Tag Item X is a cell and the # can either be an image, a cell or a view. It just needs to these are hash tags.
My current attempt at a tag cloud looks like this -
I cannot work out how to offset the section items and insert a view or any kind into that space.
I did try a hack or sorts, in that each cell contained the icon and a label, I then hide the icon on every cell after the first. This did not work however as Tag Item 3 would wrap underneath the icon and there were some re use issues also.
How can I achieve this UI please?
I believe I may need to render a nested group, 1 cell in the first with the icon and mu tags in the trailing group? I cannot make this work though.
import UIKit
final class CustomCell: UICollectionViewCell {
let label = UILabel(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.font = .systemFont(ofSize: 16)
label.textColor = .white
label.textAlignment = .center
label.sizeToFit()
contentView.addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8)
])
}
required init?(coder: NSCoder) {
return nil
}
}
protocol SectionData {
var text: String { get }
}
struct DummyData: SectionData {
let text: String
}
enum SectionType: Int, CaseIterable {
case single
case double
case carousel
case tags
}
struct Section {
let id: Int
let type: SectionType
let title: String?
let subtitle: String?
let data: [SectionData]
}
class ViewController: UIViewController {
private var items: [Section] = [] {
didSet { collectionView.reloadData() }
}
private(set) lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: view.frame, collectionViewLayout: makeLayout())
collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
collectionView.backgroundColor = .systemBackground
collectionView.dataSource = self
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
items = [
Section(id: 0, type: .single, title: nil, subtitle: nil, data: Array(0...3).map { index in DummyData(text: "List Item \(index)") }),
Section(id: 1, type: .carousel, title: nil, subtitle: nil, data: Array(0...6).map { index in DummyData(text: "Carousel Item \(index)") }),
Section(id: 2, type: .tags, title: nil, subtitle: nil, data: Array(0...15).map { index in DummyData(text: "Tag Item \(index)") })
]
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items[section].data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = items[indexPath.section].data[indexPath.item]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.label.text = model.text
cell.label.sizeToFit()
cell.backgroundColor = indexPath.item % 2 == 0 ? .darkGray : .lightGray
return cell
}
}
extension ViewController {
func makeLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { [weak self] index, env in
guard let self = self else { return nil }
let section = self.items[index]
switch section.type {
case .single: return self.makeSingleSection()
case .carousel: return self.makeCarouselSection()
case .tags: return self.makeTagSection()
default: return nil
}
}
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 20
layout.configuration = config
return layout
}
func makeSingleSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
layoutGroup.interItemSpacing = .fixed(12)
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
layoutSection.interGroupSpacing = 8
return layoutSection
}
func makeCarouselSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = .init(top: 0, leading: 8, bottom: 0, trailing: 8)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.83), heightDimension: .estimated(350))
let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.orthogonalScrollingBehavior = .groupPaging
layoutSection.contentInsets = .init(top: 0, leading: 8, bottom: 0, trailing: 24)
return layoutSection
}
func makeTagSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(100), heightDimension: .absolute(36))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: itemSize.heightDimension)
let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
layoutGroup.interItemSpacing = .fixed(8)
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
layoutSection.interGroupSpacing = 8
return layoutSection
}
}
I would use a NSCollectionLayoutBoundarySupplementaryItem on the leading edge.
You should be able to position it correctly using the absoluteOffset value.
func makeTagSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(100), heightDimension: .absolute(36))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: itemSize.heightDimension)
let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
layoutGroup.interItemSpacing = .fixed(8)
layoutGroup.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(44), top: nil, trailing: nil, bottom: nil)
let leftSize = NSCollectionLayoutSize(widthDimension: .absolute(36), heightDimension: .absolute(36))
let left = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: leftSize, elementKind: ViewController.leadingKind, alignment: .topLeading, absoluteOffset: .init(x: 0, y: 36))
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
layoutSection.interGroupSpacing = 8
layoutSection.boundarySupplementaryItems = [left]
return layoutSection
}
You can simply create a UICollectionReusableView to represent your icon.
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)
}()