Open1

CompositionalLayoutでよく使うextension

崎山圭崎山圭

import UIKit.UICollectionViewLayout

extension UICollectionViewCompositionalLayout {
    static func verticalList(
        height: CGFloat,
        itemContentInsets: NSDirectionalEdgeInsets = .zero,
        itemEdgeSpacing: NSCollectionLayoutEdgeSpacing = .init(leading: nil, top: nil, trailing: nil, bottom: nil),
        groupContentInsets: NSDirectionalEdgeInsets = .zero,
        sectionContentInsets: NSDirectionalEdgeInsets = .zero
    ) -> UICollectionViewCompositionalLayout {
        verticalList(
            height: {
                height
            },
            itemContentInsets: itemContentInsets,
            itemEdgeSpacing: itemEdgeSpacing,
            groupContentInsets: groupContentInsets,
            sectionContentInsets: sectionContentInsets
        )
    }

    static func verticalList(
        heightDimension: NSCollectionLayoutDimension,
        itemContentInsets: NSDirectionalEdgeInsets = .zero,
        itemEdgeSpacing: NSCollectionLayoutEdgeSpacing = .init(leading: nil, top: nil, trailing: nil, bottom: nil),
        groupContentInsets: NSDirectionalEdgeInsets = .zero,
        sectionContentInsets: NSDirectionalEdgeInsets = .zero
    ) -> UICollectionViewCompositionalLayout {

        verticalList(
            heightDimension: { heightDimension },
            itemContentInsets: itemContentInsets,
            itemEdgeSpacing: itemEdgeSpacing,
            groupContentInsets: groupContentInsets,
            sectionContentInsets: sectionContentInsets)

    }

    static func verticalList(
        height: @escaping (() -> CGFloat),
        itemContentInsets: NSDirectionalEdgeInsets = .zero,
        itemEdgeSpacing: NSCollectionLayoutEdgeSpacing = .init(leading: nil, top: nil, trailing: nil, bottom: nil),
        groupContentInsets: NSDirectionalEdgeInsets = .zero,
        sectionContentInsets: NSDirectionalEdgeInsets = .zero
    ) -> UICollectionViewCompositionalLayout {

        verticalList(
            heightDimension: { .absolute(height()) },
            itemContentInsets: itemContentInsets,
            itemEdgeSpacing: itemEdgeSpacing,
            groupContentInsets: groupContentInsets,
            sectionContentInsets: sectionContentInsets)
    }

    static func verticalList(
        heightDimension: @escaping (() -> NSCollectionLayoutDimension),
        itemContentInsets: NSDirectionalEdgeInsets = .zero,
        itemEdgeSpacing: NSCollectionLayoutEdgeSpacing = .init(leading: nil, top: nil, trailing: nil, bottom: nil),
        groupContentInsets: NSDirectionalEdgeInsets = .zero,
        sectionContentInsets: NSDirectionalEdgeInsets = .zero
    ) -> UICollectionViewCompositionalLayout {
        UICollectionViewCompositionalLayout { _, _ -> NSCollectionLayoutSection? in

            let itemSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: heightDimension()
            )
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = itemContentInsets
            item.edgeSpacing = itemEdgeSpacing

            let groupSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: heightDimension()
            )
            let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
            group.contentInsets = groupContentInsets

            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = sectionContentInsets

            return section
        }
    }

    static func horizontalList(
        width: CGFloat,
        itemContentInsets: NSDirectionalEdgeInsets = .zero,
        itemEdgeSpacing: NSCollectionLayoutEdgeSpacing = .init(leading: nil, top: nil, trailing: nil, bottom: nil),
        groupContentInsets: NSDirectionalEdgeInsets = .zero,
        sectionContentInsets: NSDirectionalEdgeInsets = .zero
    ) -> UICollectionViewCompositionalLayout {
        let configuration = UICollectionViewCompositionalLayoutConfiguration()
        configuration.scrollDirection = .horizontal

        let sectionProvider = { () -> NSCollectionLayoutSection in
            let itemSize = NSCollectionLayoutSize(
                widthDimension: .absolute(width),
                heightDimension: .fractionalHeight(1.0)
            )
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = itemContentInsets
            item.edgeSpacing = itemEdgeSpacing

            let groupSize = NSCollectionLayoutSize(
                widthDimension: .estimated(width),
                heightDimension: .fractionalHeight(1.0)
            )
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
            group.contentInsets = groupContentInsets

            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = sectionContentInsets

            return section
        }()

        return UICollectionViewCompositionalLayout(section: sectionProvider, configuration: configuration)
    }

    private static func createHeader(height: CGFloat) -> NSCollectionLayoutBoundarySupplementaryItem? {
        guard height > 0 else { return nil }
        return NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(height)
            ),
            elementKind: UICollectionView.elementKindSectionHeader,
            alignment: .top
        )
    }

    private static func createFooter(height: CGFloat) -> NSCollectionLayoutBoundarySupplementaryItem? {
        guard height > 0 else { return nil }
        return NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(height)
            ),
            elementKind: UICollectionView.elementKindSectionFooter,
            alignment: .bottom
        )
    }

    static func waterfall(contentInsets: NSDirectionalEdgeInsets = .init(top: 8, leading: 8, bottom: 8, trailing: 8),
                          headerHeight: CGFloat = 0,
                          footerHeight: CGFloat = 0,
                          horizontalSpace: CGFloat = 8,
                          verticalSpace: CGFloat = 8,
                          numberOfColumn: CGFloat = 1,
                          numberOfItemsInSection: @escaping ((Int) -> Int),
                          cellHeight: @escaping (((width: CGFloat, index: Int)) -> CGFloat)) -> UICollectionViewLayout {
        UICollectionViewCompositionalLayout { section, environment -> NSCollectionLayoutSection? in

            // 各列の最後のitemのmaxYを保存するための辞書
            // 最初は全て0で初期化する
            var itemMaxYPerColumns: [Int: CGFloat] = Dictionary(
                uniqueKeysWithValues: (0 ..< Int(numberOfColumn)).map { ($0, 0) }
            )

            // セルひとつの横幅
            let width = (environment.container.effectiveContentSize.width
                            - (contentInsets.leading + contentInsets.trailing)
                            - (numberOfColumn - 1) * horizontalSpace) / numberOfColumn

            let items: [NSCollectionLayoutGroupCustomItem] = (0 ..< numberOfItemsInSection(section)).map { idx in
                // セルひとつの縦幅
                let height = cellHeight((width, idx))

                // セルの配置座標を計算
                let currentColumn = idx % Int(numberOfColumn)
                let currentRow = idx / Int(numberOfColumn)
                let preItemMaxY = (itemMaxYPerColumns[currentColumn] ?? 0.0)
                let y = preItemMaxY + (currentRow == 0 ? 0.0 : verticalSpace)
                let x = environment.container.contentInsets.leading + width * CGFloat(currentColumn) + horizontalSpace * CGFloat(currentColumn)

                // セルの配置frame
                let frame = CGRect(x: x, y: y, width: width, height: height)
                itemMaxYPerColumns[currentColumn] = frame.maxY
                let item = NSCollectionLayoutGroupCustomItem(frame: frame)
                return item
            }

            let layoutSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: items.isEmpty ? .fractionalHeight(1.0) : .absolute(items.last!.frame.maxY)
            )

            // group
            let group = NSCollectionLayoutGroup.custom(layoutSize: layoutSize) { _ in
                items
            }

            // なぜかgroupが反応しない
            //            group.contentInsets = .init(top: 0, leading: contentInsets.leading, bottom: 0, trailing: contentInsets.trailing)
            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = contentInsets

            /*
             header footerの設定
             */
            let boundaryItems: [NSCollectionLayoutBoundarySupplementaryItem] =
                [createHeader(height: headerHeight), createFooter(height: footerHeight)]
                .compactMap { $0 }
            if !boundaryItems.isEmpty {
                // セクションに登録
                section.boundarySupplementaryItems = boundaryItems
            }

            return section
        }
    }
}