Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ohtwo/385a5acb280a426d8c5283623f5ce68f to your computer and use it in GitHub Desktop.
Save ohtwo/385a5acb280a426d8c5283623f5ce68f to your computer and use it in GitHub Desktop.
import UIKit
public extension NSCollectionLayoutAnchor {
struct Offset {
fileprivate var absolute: CGPoint?
fileprivate var fractional: CGPoint?
private init(absolute: CGPoint?, fractional: CGPoint?) {
self.absolute = absolute
self.fractional = fractional
}
public static func absolute(x: CGFloat) -> Offset {
absolute(x: x, y: 0)
}
public static func absolute(y: CGFloat) -> Offset {
absolute(x: 0, y: y)
}
public static func absolute(x: CGFloat, y: CGFloat) -> Offset {
Offset(absolute: CGPoint(x: x, y: y), fractional: nil)
}
public static func fractional(x: CGFloat) -> Offset {
fractional(x: x, y: 0)
}
public static func fractional(y: CGFloat) -> Offset {
fractional(x: 0, y: y)
}
public static func fractional(x: CGFloat, y: CGFloat) -> Offset {
Offset(absolute: nil, fractional: CGPoint(x: x, y: y))
}
}
}
// MARK: - Supplementary Item
public class LayoutSupplementaryItem {
fileprivate var width: NSCollectionLayoutDimension
fileprivate var height: NSCollectionLayoutDimension
fileprivate var elementKind: String
fileprivate var containerAnchor: NSCollectionLayoutAnchor = NSCollectionLayoutAnchor(edges: [])
fileprivate var itemAnchor: NSCollectionLayoutAnchor = NSCollectionLayoutAnchor(edges: [])
fileprivate var zIndex: Int? = nil
public init(
_ elementKind: String,
width: NSCollectionLayoutDimension = .fractionalWidth(0.2),
height: NSCollectionLayoutDimension = .fractionalHeight(0.2)
) {
self.width = width
self.height = height
self.elementKind = elementKind
}
public func containerAnchor(_ edges: NSDirectionalRectEdge, offset: NSCollectionLayoutAnchor.Offset = .absolute(x: 0, y: 0)) -> LayoutSupplementaryItem {
self.containerAnchor = makeAnchor(edges: edges, offset: offset)
return self
}
public func itemAnchor(_ edges: NSDirectionalRectEdge, offset: NSCollectionLayoutAnchor.Offset = .absolute(x: 0, y: 0)) -> LayoutSupplementaryItem {
self.itemAnchor = makeAnchor(edges: edges, offset: offset)
return self
}
private func makeAnchor(edges: NSDirectionalRectEdge, offset: NSCollectionLayoutAnchor.Offset = .absolute(x: 0, y: 0)) -> NSCollectionLayoutAnchor {
if let fractionalOffset = offset.fractional {
return NSCollectionLayoutAnchor(edges: edges, fractionalOffset: fractionalOffset)
} else if let absoluteOffset = offset.absolute {
return NSCollectionLayoutAnchor(edges: edges, absoluteOffset: absoluteOffset)
} else {
return NSCollectionLayoutAnchor(edges: edges, absoluteOffset: CGPoint(x: 0, y: 0))
}
}
public func zIndex(_ zIndex: Int) -> LayoutSupplementaryItem {
self.zIndex = zIndex
return self
}
}
fileprivate extension NSCollectionLayoutSupplementaryItem {
convenience init(_ config: LayoutSupplementaryItem) {
self.init(layoutSize: NSMakeSize(config.width, config.height), elementKind: config.elementKind, containerAnchor: config.containerAnchor, itemAnchor: config.itemAnchor)
if let zIndex = config.zIndex {
self.zIndex = zIndex
}
}
}
@_functionBuilder
public struct LayoutSupplementaryItemBuilder {
public static func buildBlock(_ items: LayoutSupplementaryItem...) -> [LayoutSupplementaryItem] {
items
}
}
// MARK: - Boundary Supplementary Item
public class LayoutBoundarySupplementaryItem {
fileprivate var width: NSCollectionLayoutDimension
fileprivate var height: NSCollectionLayoutDimension
fileprivate var elementKind: String
fileprivate var alignment: NSRectAlignment = .none
fileprivate var offset: CGPoint = .zero
fileprivate var pinToVisibleBounds: Bool = false
fileprivate var extendsBoundary: Bool = false
public init(
_ elementKind: String,
width: NSCollectionLayoutDimension = .fractionalWidth(0.2),
height: NSCollectionLayoutDimension = .fractionalHeight(0.2)
) {
self.elementKind = elementKind
self.width = width
self.height = height
}
public func alignment(_ alignment: NSRectAlignment) -> LayoutBoundarySupplementaryItem {
self.alignment = alignment
return self
}
public func extendsBoundary(_ flag: Bool) -> LayoutBoundarySupplementaryItem {
self.extendsBoundary = flag
return self
}
public func absoluteOffset(x: CGFloat = 0, y: CGFloat = 0) -> LayoutBoundarySupplementaryItem {
self.offset = CGPoint(x: x, y: y)
return self
}
public func pinToVisibleBounds(_ flag: Bool) -> LayoutBoundarySupplementaryItem {
self.pinToVisibleBounds = flag
return self
}
}
fileprivate extension NSCollectionLayoutBoundarySupplementaryItem {
convenience init(_ config: LayoutBoundarySupplementaryItem) {
self.init(layoutSize: NSMakeSize(config.width, config.height), elementKind: config.elementKind, alignment: config.alignment, absoluteOffset: config.offset)
self.extendsBoundary = config.extendsBoundary
self.pinToVisibleBounds = config.pinToVisibleBounds
}
}
@_functionBuilder
public struct LayoutBoundarySupplementaryItemBuilder {
public static func buildBlock(_ items: LayoutBoundarySupplementaryItem...) -> [LayoutBoundarySupplementaryItem] {
items
}
}
// MARK: - Decoration Item
public class LayoutDecorationItem {
fileprivate enum Kind {
case background
}
fileprivate var elementKind: String
fileprivate var kind: Kind
fileprivate var zIndex: Int? = nil
private init(kind: Kind, elementKind: String) {
self.kind = kind
self.elementKind = elementKind
}
func zIndex(_ zIndex: Int) -> LayoutDecorationItem {
self.zIndex = zIndex
return self
}
}
fileprivate extension NSCollectionLayoutDecorationItem {
static func convert(from config: LayoutDecorationItem) -> NSCollectionLayoutDecorationItem {
var item: NSCollectionLayoutDecorationItem
switch config.kind {
case .background:
item = NSCollectionLayoutDecorationItem.background(elementKind: config.elementKind)
}
if let zIndex = config.zIndex {
item.zIndex = zIndex
}
return item
}
}
@_functionBuilder
struct LayoutDecorationItemBuilder {
public static func buildBlock(_ items: LayoutDecorationItem...) -> [LayoutDecorationItem] {
items
}
}
// MARK: - Item
public class LayoutItem {
fileprivate var width: NSCollectionLayoutDimension
fileprivate var height: NSCollectionLayoutDimension
fileprivate var contentInsets: NSDirectionalEdgeInsets = .zero
fileprivate var edgeSpacing: NSCollectionLayoutEdgeSpacing? = nil
fileprivate var supplementaryItems: [NSCollectionLayoutSupplementaryItem]
public init(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1)
) {
self.width = width
self.height = height
self.supplementaryItems = []
}
public init(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
@LayoutSupplementaryItemBuilder supplementaryItemsBuilder: () -> [LayoutSupplementaryItem]
) {
self.width = width
self.height = height
self.supplementaryItems = supplementaryItemsBuilder().map { NSCollectionLayoutSupplementaryItem($0) }
}
public init(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
@LayoutSupplementaryItemBuilder supplementaryItem: () -> LayoutSupplementaryItem
) {
self.width = width
self.height = height
self.supplementaryItems = [NSCollectionLayoutSupplementaryItem(supplementaryItem())]
}
public func contentInsets(top: CGFloat = 0, leading: CGFloat = 0, bottom: CGFloat = 0, trailing: CGFloat = 0) -> LayoutItem {
self.contentInsets = NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing)
return self
}
public func edgeSpacing(top: NSCollectionLayoutSpacing? = nil, leading: NSCollectionLayoutSpacing? = nil, bottom: NSCollectionLayoutSpacing? = nil, trailing: NSCollectionLayoutSpacing? = nil) -> LayoutItem {
self.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom)
return self
}
}
fileprivate extension NSCollectionLayoutItem {
convenience init(_ config: LayoutItem) {
self.init(layoutSize: NSMakeSize(config.width, config.height), supplementaryItems: config.supplementaryItems)
self.contentInsets = config.contentInsets
self.edgeSpacing = config.edgeSpacing
}
}
@_functionBuilder
public struct LayoutItemBuilder {
public static func buildBlock(_ items: LayoutItem...) -> [LayoutItem] {
items
}
}
// MARK: - Group
public class LayoutGroup {
fileprivate enum Kind {
case vertical, horizontal, custom
}
fileprivate var kind: Kind
fileprivate var width: NSCollectionLayoutDimension
fileprivate var height: NSCollectionLayoutDimension
fileprivate var subitems: [NSCollectionLayoutItem]? = nil
fileprivate var count: Int? = nil
fileprivate var interItemSpacing: NSCollectionLayoutSpacing? = nil
fileprivate var supplementaryItems: [NSCollectionLayoutSupplementaryItem] = []
fileprivate var itemProvider: NSCollectionLayoutGroupCustomItemProvider? = nil
// horizontal
public static func horizontal(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
@LayoutItemBuilder itemsBuilder: () -> [LayoutItem]
) -> LayoutGroup {
horizontal(width: width, height: height, supplementaryItemsBuilder: { [] }, itemsBuilder: itemsBuilder)
}
public static func horizontal(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
count: Int? = nil,
@LayoutItemBuilder itemsBuilder: () -> LayoutItem
) -> LayoutGroup {
horizontal(width: width, height: height, count: count, supplementaryItemsBuilder: { [] }, itemsBuilder: itemsBuilder)
}
public static func horizontal(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
@LayoutSupplementaryItemBuilder supplementaryItemsBuilder: () -> LayoutSupplementaryItem,
@LayoutItemBuilder itemsBuilder: () -> [LayoutItem]
) -> LayoutGroup {
horizontal(width: width, height: height, supplementaryItemsBuilder: { [supplementaryItemsBuilder()] }, itemsBuilder: itemsBuilder)
}
public static func horizontal(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
count: Int? = nil,
@LayoutSupplementaryItemBuilder supplementaryItemsBuilder: () -> LayoutSupplementaryItem,
@LayoutItemBuilder itemsBuilder: () -> LayoutItem
) -> LayoutGroup {
horizontal(width: width, height: height, count: count, supplementaryItemsBuilder: { [supplementaryItemsBuilder()] }, itemsBuilder: itemsBuilder)
}
public static func horizontal(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
@LayoutSupplementaryItemBuilder supplementaryItemsBuilder: () -> [LayoutSupplementaryItem],
@LayoutItemBuilder itemsBuilder: () -> [LayoutItem]
) -> LayoutGroup {
let group = LayoutGroup(.horizontal, width: width, height: height)
group.subitems = itemsBuilder().map { NSCollectionLayoutItem($0) }
group.supplementaryItems = supplementaryItemsBuilder().map { NSCollectionLayoutSupplementaryItem($0) }
return group
}
public static func horizontal(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
count: Int? = nil,
@LayoutSupplementaryItemBuilder supplementaryItemsBuilder: () -> [LayoutSupplementaryItem],
@LayoutItemBuilder itemsBuilder: () -> LayoutItem
) -> LayoutGroup {
let group = LayoutGroup(.horizontal, width: width, height: height)
group.count = count
group.subitems = [NSCollectionLayoutItem(itemsBuilder())]
group.supplementaryItems = supplementaryItemsBuilder().map { NSCollectionLayoutSupplementaryItem($0) }
return group
}
// vertical
public static func vertical(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
@LayoutItemBuilder itemsBuilder: () -> [LayoutItem]
) -> LayoutGroup {
vertical(width: width, height: height, supplementaryItemsBuilder: { [] }, itemsBuilder: itemsBuilder)
}
public static func vertical(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
count: Int? = nil,
@LayoutItemBuilder itemsBuilder: () -> LayoutItem
) -> LayoutGroup {
vertical(width: width, height: height, count: count, supplementaryItemsBuilder: { [] }, itemsBuilder: itemsBuilder)
}
public static func vertical(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
@LayoutSupplementaryItemBuilder supplementaryItemsBuilder: () -> LayoutSupplementaryItem,
@LayoutItemBuilder itemsBuilder: () -> [LayoutItem]
) -> LayoutGroup {
vertical(width: width, height: height, supplementaryItemsBuilder: { [supplementaryItemsBuilder()] }, itemsBuilder: itemsBuilder)
}
public static func vertical(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
count: Int? = nil,
@LayoutSupplementaryItemBuilder supplementaryItemsBuilder: () -> LayoutSupplementaryItem,
@LayoutItemBuilder itemsBuilder: () -> LayoutItem
) -> LayoutGroup {
vertical(width: width, height: height, count: count, supplementaryItemsBuilder: { [supplementaryItemsBuilder()] }, itemsBuilder: itemsBuilder)
}
public static func vertical(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
@LayoutSupplementaryItemBuilder supplementaryItemsBuilder: () -> [LayoutSupplementaryItem],
@LayoutItemBuilder itemsBuilder: () -> [LayoutItem]
) -> LayoutGroup {
let group = LayoutGroup(.vertical, width: width, height: height)
group.subitems = itemsBuilder().map { NSCollectionLayoutItem($0) }
group.supplementaryItems = supplementaryItemsBuilder().map { NSCollectionLayoutSupplementaryItem($0) }
return group
}
public static func vertical(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
count: Int? = nil,
@LayoutSupplementaryItemBuilder supplementaryItemsBuilder: () -> [LayoutSupplementaryItem],
@LayoutItemBuilder itemsBuilder: () -> LayoutItem
) -> LayoutGroup {
let group = LayoutGroup(.vertical, width: width, height: height)
group.count = count
group.subitems = [NSCollectionLayoutItem(itemsBuilder())]
group.supplementaryItems = supplementaryItemsBuilder().map { NSCollectionLayoutSupplementaryItem($0) }
return group
}
// custom
public static func custom(
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1),
itemPrivider: @escaping NSCollectionLayoutGroupCustomItemProvider
) -> LayoutGroup {
let group = LayoutGroup(.custom, width: width, height: height)
group.itemProvider = itemPrivider
return group
}
private init(
_ kind: Kind,
width: NSCollectionLayoutDimension = .fractionalWidth(1),
height: NSCollectionLayoutDimension = .fractionalHeight(1)
) {
self.kind = kind
self.width = width
self.height = height
}
public func interItemSpacing(_ spacing: NSCollectionLayoutSpacing) -> LayoutGroup {
self.interItemSpacing = spacing
return self
}
}
fileprivate extension NSCollectionLayoutGroup {
static func convert(from config: LayoutGroup) -> NSCollectionLayoutGroup {
var group: NSCollectionLayoutGroup
switch config.kind {
case .horizontal:
if let count = config.count {
group = NSCollectionLayoutGroup.horizontal(layoutSize: NSMakeSize(config.width, config.height), subitem: config.subitems?.first ?? NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(0), heightDimension: .absolute(0))), count: count)
} else {
group = NSCollectionLayoutGroup.horizontal(layoutSize: NSMakeSize(config.width, config.height), subitems: config.subitems ?? [])
}
case .vertical:
if let count = config.count {
group = NSCollectionLayoutGroup.vertical(layoutSize: NSMakeSize(config.width, config.height), subitem: config.subitems?.first ?? NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(0), heightDimension: .absolute(0))), count: count)
} else {
group = NSCollectionLayoutGroup.vertical(layoutSize: NSMakeSize(config.width, config.height), subitems: config.subitems ?? [])
}
case .custom:
group = NSCollectionLayoutGroup.custom(layoutSize: NSMakeSize(config.width, config.height), itemProvider: config.itemProvider ?? { _ in [] })
}
group.interItemSpacing = config.interItemSpacing
group.supplementaryItems = config.supplementaryItems
return group
}
}
@_functionBuilder
public struct LayoutGroupBuilder {
public static func buildBlock(_ items: LayoutGroup...) -> [LayoutGroup] {
items
}
}
// MARK: - Section
public class LayoutSection {
fileprivate var contentInsets: NSDirectionalEdgeInsets = .zero
fileprivate var interGroupSpacing: CGFloat = 0
fileprivate var orthogonalScrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior = .none
fileprivate var supplementariesFollowContentInsets: Bool = false
fileprivate var visibleItemsInvalidationHandler: NSCollectionLayoutSectionVisibleItemsInvalidationHandler? = nil
fileprivate var decorationItems: [NSCollectionLayoutDecorationItem] = []
fileprivate var boundarySupplementaryItems: [NSCollectionLayoutBoundarySupplementaryItem] = []
fileprivate var group: NSCollectionLayoutGroup
public init(
_ scrollBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior = .none,
@LayoutGroupBuilder groupBuilder: () -> LayoutGroup
) {
self.orthogonalScrollingBehavior = scrollBehavior
self.group = NSCollectionLayoutGroup.convert(from: groupBuilder())
}
public func contentInsets(top: CGFloat = 0, leading: CGFloat = 0, bottom: CGFloat = 0, trailing: CGFloat = 0) -> LayoutSection {
self.contentInsets = NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing)
return self
}
public func interGroupSpacing(_ spacing: CGFloat) -> LayoutSection {
self.interGroupSpacing = spacing
return self
}
public func supplementariesFollowContentInsets(_ flag: Bool) -> LayoutSection {
self.supplementariesFollowContentInsets = flag
return self
}
public func visibleItemsInvalidationHandler(_ handler: @escaping NSCollectionLayoutSectionVisibleItemsInvalidationHandler) -> LayoutSection {
self.visibleItemsInvalidationHandler = handler
return self
}
public func decorationItems(@LayoutDecorationItemBuilder _ itemsBuilder: () -> [LayoutDecorationItem]) -> LayoutSection {
self.decorationItems = itemsBuilder().map { NSCollectionLayoutDecorationItem.convert(from: $0) }
return self
}
public func decorationItems(@LayoutDecorationItemBuilder _ itemBuilder: () -> LayoutDecorationItem) -> LayoutSection {
self.decorationItems = [NSCollectionLayoutDecorationItem.convert(from: itemBuilder())]
return self
}
public func boundarySupplementaryItems(@LayoutBoundarySupplementaryItemBuilder _ itemsBuilder: () -> [LayoutBoundarySupplementaryItem]) -> LayoutSection {
self.boundarySupplementaryItems = itemsBuilder().map { NSCollectionLayoutBoundarySupplementaryItem($0) }
return self
}
public func boundarySupplementaryItems(@LayoutBoundarySupplementaryItemBuilder _ itemBuilder: () -> LayoutBoundarySupplementaryItem) -> LayoutSection {
self.boundarySupplementaryItems = [NSCollectionLayoutBoundarySupplementaryItem(itemBuilder())]
return self
}
}
fileprivate extension NSCollectionLayoutSection {
convenience init(_ config: LayoutSection) {
self.init(group: config.group)
self.boundarySupplementaryItems = config.boundarySupplementaryItems
self.contentInsets = config.contentInsets
self.decorationItems = config.decorationItems
self.interGroupSpacing = config.interGroupSpacing
self.orthogonalScrollingBehavior = config.orthogonalScrollingBehavior
self.supplementariesFollowContentInsets = config.supplementariesFollowContentInsets
self.visibleItemsInvalidationHandler = config.visibleItemsInvalidationHandler
}
}
@_functionBuilder
public struct LayoutSectionBuilder {
public static func buildBlock(_ items: LayoutSection...) -> [LayoutSection] {
items
}
}
// MARK: - Layout
public extension UICollectionViewCompositionalLayout {
convenience init(
scrollDirection: UICollectionView.ScrollDirection = .vertical,
@LayoutSectionBuilder sectionBuilder: () -> LayoutSection
) {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = scrollDirection
self.init(section: NSCollectionLayoutSection(sectionBuilder()), configuration: config)
}
convenience init(
scrollDirection: UICollectionView.ScrollDirection = .vertical,
sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider
) {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = scrollDirection
self.init(sectionProvider: sectionProvider, configuration: config)
}
func interSectionSpacing(_ spacing: CGFloat) -> UICollectionViewCompositionalLayout {
self.configuration.interSectionSpacing = spacing
return self
}
func boundarySupplementaryItems(@LayoutBoundarySupplementaryItemBuilder _ itemsBuilder: () -> [LayoutBoundarySupplementaryItem]) -> UICollectionViewCompositionalLayout {
self.configuration.boundarySupplementaryItems = itemsBuilder().map { NSCollectionLayoutBoundarySupplementaryItem($0) }
return self
}
func boundarySupplementaryItems(@LayoutBoundarySupplementaryItemBuilder _ itemBuilder: () -> LayoutBoundarySupplementaryItem) -> UICollectionViewCompositionalLayout {
self.configuration.boundarySupplementaryItems = [NSCollectionLayoutBoundarySupplementaryItem(itemBuilder())]
return self
}
}
// MARK:- Helper
fileprivate func NSMakeSize(_ widthDimension: NSCollectionLayoutDimension, _ heightDimension: NSCollectionLayoutDimension) -> NSCollectionLayoutSize {
NSCollectionLayoutSize(widthDimension: widthDimension, heightDimension: heightDimension)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment