4

I am trying to create a 'tag cloud' section using a UICollectionViewCompositionalLayout

I would like a full width section of items followed by a section that wraps in this fashion

example

Essential HR, Financial Wellbeing and Employee engagement would each be a cell inside a section. Please disregard the other UI elements.

This section would need to support multiple rows also as there could be anything from 1 to 15 tags.

I have attempted it but am not able to create that wrapping effect, not have a width for each item, using the intrinsicContentSize of each item.

enter image description here

My current attempt so far is

import UIKit

final class CustomCell: UICollectionViewCell {
  let label = UILabel(frame: .zero)

  override init(frame: CGRect) {
    super.init(frame: frame)
    label.translatesAutoresizingMaskIntoConstraints = false
    contentView.addSubview(label)
    NSLayoutConstraint.activate([
      label.topAnchor.constraint(equalTo: contentView.topAnchor),
      label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
      label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
      label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
    ])
  }

  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: "Item \(index)") }),
      Section(id: 1, type: .tags, title: nil, subtitle: nil, data: Array(0...15).map { index in DummyData(text: "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.backgroundColor = indexPath.item % 2 == 0 ? .darkGray : .lightGray
    cell.label.text = model.text
    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 .tags: return self.makeTagSection(count: section.data.count)
        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])
    let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
    return layoutSection
  }

  func makeTagSection(count: Int) -> 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, subitem: layoutItem, count: count)
    let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
    return layoutSection
  }
}

I would really appreciate any help I can get understanding how to achieve this layout please.

Harry Blue
  • 4,202
  • 10
  • 39
  • 78

1 Answers1

2

You can create that section layout, there are a few errors in your factory method however.

Something like this will produce the effect I believe you want -

  func makeTagSection(count: Int) -> NSCollectionLayoutSection {
    let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(100), heightDimension: .absolute(32))
    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
  }

enter image description here

nodediggity
  • 2,458
  • 1
  • 9
  • 12
  • Hi! Do you know a way to have this, but with the tags centered in the collection? [https://stackoverflow.com/questions/64965485/uicollectionviewcompositionallayout-center-items-in-sections-or-groups](just like in this question I asked). Thanks anyway! – nknr Nov 23 '20 at 09:24
  • how do you handle case where the text for a particular cell is very long and the label needs to have multiple lines and have maximum width no larger than the width of the group/section? – caa5042 May 18 '23 at 18:37