我怎样才能用这样的图像/图标/视图来偏移合成布局

How can I offset a Compositional Layout with an image / icon / view like this

我正在尝试实现 'tag cloud' 效果,应该看起来像这样 -

每个 Tag Item X 是一个单元格,# 可以是图像、单元格或视图。它只需要这些是哈希标签。

我目前对标签云的尝试是这样的 -

我不知道如何偏移部分项目并将视图或任何类型插入其中 space。

我确实尝试过破解或排序,因为每个单元格都包含图标和标签,然后我在第一个单元格之后隐藏每个单元格上的图标。但是,这不起作用,因为 Tag Item 3 会包裹在图标下方,并且还存在一些重用问题。

请问我怎样才能做到这一点UI?

我相信我可能需要渲染一个嵌套组,第一个单元格中有 icon 和尾随组中的 mu 标签?不过我做不到。


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
  }
}

我会在前沿使用 NSCollectionLayoutBoundarySupplementaryItem

您应该能够使用 absoluteOffset 值正确定位它。

  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
  }

您可以简单地创建一个 UICollectionReusableView 来代表您的图标。