{"id":18264752,"url":"https://github.com/reactcomponentkit/listkit","last_synced_at":"2025-04-04T21:30:48.979Z","repository":{"id":46916004,"uuid":"407201764","full_name":"ReactComponentKit/ListKit","owner":"ReactComponentKit","description":"DSL for UICollectionViewCompositionalLayout","archived":false,"fork":false,"pushed_at":"2021-09-21T15:04:17.000Z","size":76,"stargazers_count":31,"open_issues_count":1,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-20T19:17:04.706Z","etag":null,"topics":["dsl","ios","uicollectionview","uicollectionviewcompositionallayout","uicollectionviewdiffabledatasource","uikit"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ReactComponentKit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-09-16T14:43:18.000Z","updated_at":"2024-12-14T16:49:00.000Z","dependencies_parsed_at":"2022-08-25T11:01:08.964Z","dependency_job_id":null,"html_url":"https://github.com/ReactComponentKit/ListKit","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactComponentKit%2FListKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactComponentKit%2FListKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactComponentKit%2FListKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactComponentKit%2FListKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ReactComponentKit","download_url":"https://codeload.github.com/ReactComponentKit/ListKit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247252019,"owners_count":20908611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsl","ios","uicollectionview","uicollectionviewcompositionallayout","uicollectionviewdiffabledatasource","uikit"],"created_at":"2024-11-05T11:15:49.714Z","updated_at":"2025-04-04T21:30:48.617Z","avatar_url":"https://github.com/ReactComponentKit.png","language":"Swift","readme":"\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://raw.githubusercontent.com/ReactComponentKit/ListKitExamples/main/arts/listkit.logo.png\" width=700\u003e\n\u003c/p\u003e\n\n# ListKit\n\n![license MIT](https://img.shields.io/cocoapods/l/ListKit.svg)\n![Platform](https://img.shields.io/badge/iOS-%3E%3D%2013.0-green.svg)\n[![Swift 5.4](https://img.shields.io/badge/Swift-5.4-orange.svg?style=flat)](https://developer.apple.com/swift/)\n\n*DSL for UICollectionViewCompositionalLayout!*\n\n## About\n\nListKit is DSL for building [UICollectionViewCompositionalLayout](https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayout). You can make UICollectionViewCompositionalLayout easy with ListKit. ListKit is Declarative and Component-Based. Also, ListKit supports diffable data source for UICollectionView!\n\n## Examples\n\nYou can checkout examples for ListKit at here: \n- [https://github.com/ReactComponentKit/ListKitExamples](https://github.com/ReactComponentKit/ListKitExamples)\n\n|![](https://raw.githubusercontent.com/ReactComponentKit/ListKitExamples/main/arts/ex01.gif)|![](https://raw.githubusercontent.com/ReactComponentKit/ListKitExamples/main/arts/ex02.gif)|![](https://raw.githubusercontent.com/ReactComponentKit/ListKitExamples/main/arts/ex03.gif)|![](https://raw.githubusercontent.com/ReactComponentKit/ListKitExamples/main/arts/ex04.gif)|![](https://raw.githubusercontent.com/ReactComponentKit/ListKitExamples/main/arts/ex05.gif)|![](https://raw.githubusercontent.com/ReactComponentKit/ListKitExamples/main/arts/ex06.gif)\n|:----------------------------:|:------------------------:|:------------------------:|:----------------------:|:----------------------:|:----------------------:|\n\n\n\n\u003cimg src=\"https://raw.githubusercontent.com/ReactComponentKit/ListKitExamples/main/arts/ex03.gif\" height=320 align=right\u003e\n\n```swift\nrenderer.render(of: Array(0..\u003c10)) { index in\n    Section(id: index) {\n        HGroup(width: .fractionalWidth(1.0), height: .absolute(150)) {\n            for _ in 0..\u003c3 {\n                ColorBox2Component(color: randomColor, width: .fractionalWidth(0.5), height: .fractionalHeight(1.0))\n                VGroup(of: [0, 1], width: .fractionalWidth(0.25), height: .fractionalHeight(1.0)) { _ in\n                    ColorBox2Component(color: randomColor, width: .fractionalWidth(1.0), height: .fractionalHeight(0.5))\n                }\n                VGroup(of: [0, 1], width: .fractionalWidth(0.25), height: .fractionalHeight(1.0)) { _ in\n                    ColorBox2Component(color: randomColor, width: .fractionalWidth(1.0), height: .fractionalHeight(0.5))\n                }\n            }\n        }\n    }\n    .orthogonalScrollingBehavior(.groupPaging)\n    .boundarySupplementaryItem(SectionHeaderComponent(title: \"Section \\(index + 1)\"))\n}\n```\n\n---\n\n## Layout Elements\n\n### Section\n\n```swift\nSection(id: UUID()) {\n    HGroup(width: .fractionalWidth(1.0), height: .absolute(150)) {\n        for i in 0..\u003c4 {\n            ColorBoxComponent(color: colors[i], width: .fractionalWidth(1.0/4), height: .fractionalHeight(1.0))\n        }\n    }\n}\n.contentInsets(top: 16, leading: 16, bottom: 16, trailing: 16)\n.decorationItem(SectionBackgroundComponent())\n.boundarySupplementaryItem(SectionHeaderComponent(title: \"Section \\(index + 1)\"))\n```\n\nSection is a group of data items. You can define multiple sections in a layout. In UICollectionViewCompositionalLayout, Section has only one root Group. \n\n\n### Group(HGroup and VGroup)\n\nIn UICollectionViewCompositionalLayout, individual items are grouped into Group. Group has two types which are HGroup and VGroup. HGroup layouts items in horizontaly direction. VGroup layouts items in vertically. Group can have multiple items(components in ListKit) and groups.\n\n\n```swift\nSection(id: UUID()) {\n    VGroup(of: [0, 1, 2], width: .fractionalWidth(1.0), height: .estimated(30)) { number in\n        HGroup(of: [0, 1, 2], width: .fractionalWidth(1.0), height: .absolute(100)) { index in\n            ColorBoxComponent(color: colors[(number * 3) + index], width: .fractionalWidth(1.0/3.0), height: .fractionalHeight(1.0))\n        }\n    }\n}\n```\n\n### Component\n\nComponent presents UI for the data item. It is the basic unit in ListKit. You can map a data into a component. You can define a component like below:\n\n```swift\nimport UIKit\nimport ListKit\n\nstruct ColorBoxComponent: Component {\n    var id: AnyHashable { UUID() }\n    let color: UIColor\n    let width: NSCollectionLayoutDimension\n    let height: NSCollectionLayoutDimension\n    \n    public init(color: UIColor, width: NSCollectionLayoutDimension, height: NSCollectionLayoutDimension) {\n        self.color = color\n        self.width = width\n        self.height = height\n    }\n    \n    func contentView() -\u003e UIView {\n        return UIView(frame: .zero)\n    }\n    \n    func layoutSize() -\u003e NSCollectionLayoutSize {\n        return NSCollectionLayoutSize(widthDimension: width, heightDimension: height)\n    }\n    \n    func edgeSpacing() -\u003e NSCollectionLayoutEdgeSpacing? {\n        return nil\n    }\n    \n    func contentInsets() -\u003e NSDirectionalEdgeInsets {\n        return .zero\n    }\n    \n    func render(in content: UIView) {\n        content.backgroundColor = color\n    }\n}\n```\n\nComponent has a content view which is inherited from UIVIew. You can define more complex component with it's content view.\n\n```swift\nimport UIKit\nimport SnapKit\nimport ListKit\n\nstruct EmojiBoxComponent: Component {\n    let id: AnyHashable\n    let emoji: String\n    \n    init(emoji: String) {\n        self.id = emoji\n        self.emoji = emoji\n    }\n    \n    func contentView() -\u003e EmojiBoxComponentContentView {\n        EmojiBoxComponentContentView()\n    }\n    \n    func layoutSize() -\u003e NSCollectionLayoutSize {\n        return NSCollectionLayoutSize(widthDimension: .absolute(30), heightDimension: .absolute(30))\n    }\n    \n    func edgeSpacing() -\u003e NSCollectionLayoutEdgeSpacing? {\n        return nil\n    }\n    \n    func contentInsets() -\u003e NSDirectionalEdgeInsets {\n        return .init(top: 2, leading: 2, bottom: 2, trailing: 2)\n    }\n    \n    func render(in content: EmojiBoxComponentContentView) {\n        content.label.text = emoji\n    }\n}\n\nfinal class EmojiBoxComponentContentView: UIView {\n    lazy var label: UILabel = {\n        let label = UILabel(frame: .zero)\n        label.font = UIFont.boldSystemFont(ofSize: 14)\n        label.textColor = .white\n        label.textAlignment = .center\n        return label\n    }()\n    \n    init() {\n        super.init(frame: .zero)\n        setupView()\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError()\n    }\n    \n    func setupView() {\n        addSubview(label)\n        label.snp.makeConstraints { make in\n            make.edges.equalToSuperview()\n        }\n        self.backgroundColor = .darkGray\n        self.layer.borderWidth = 3\n        self.layer.borderColor = UIColor.lightGray.cgColor\n        self.layer.cornerRadius = 8.0\n    }\n}\n```\n\n## Define Layout and Render it\n\n### Renderer\n\nYou can define layout in declarative way and render the layout with Renderer. Rederer is defined with DataSource. \n\n```swift\nvar renderer: ComposeRenderer = ComposeRenderer(dataSource: PlainDataSource())\n```\n\nAlso, you can set UICollectionViewDelegate and custom collection view cell. \n\n```swift\n\n/// Renderer's initializer.\npublic init(dataSource: DataSource, delegate: UICollectionViewDelegate? = nil, cellClass: AnyClass? = nil) {\n    ...\n}\n```\n\n[Todo example](https://github.com/ReactComponentKit/ListKitExamples/tree/main/ListKitExamples/ListKitExamples/TodoExample) use cellClass for handling swipe actions.\n\nYou can define layout and update it like below:\n\n```swift\nvar emojiList: [String] = [\"😊\"] {\n    didSet {\n        render()\n    }\n}\n\noverride func render() {\n    renderer.render(animated: true) {\n        Section(id: Sections.main) {\n            HGroup(of: emojiList, width: .fractionalWidth(1.0), height: .estimated(30)) { item in\n                EmojiBoxComponent(emoji: item)\n            }\n        }\n    }\n}\n```\n\n## DataSource\n\nListKit provides PlainDataSource and DiffableDataSource. PlainDataSource is used for UICollectionView that uses UICollectionViewFlowLayout. DiffableDataSource is used for UICollectionView that uses UICollectionViewDiffableDataSource and NSDiffableDataSourceSnapshot. The [emoji example](https://github.com/ReactComponentKit/ListKitExamples/tree/main/ListKitExamples/ListKitExamples/EmojiExample) and [Todo example](https://github.com/ReactComponentKit/ListKitExamples/tree/main/ListKitExamples/ListKitExamples/TodoExample) use DiffableDataSource\n\n### Customizing DataSource\n\nYou can customize data source like below:\n\n```swift\nimport UIKit\nimport ListKit\nimport SwipeCellKit\n\nclass TodoDataSource: DiffableDataSource, SwipeCollectionViewCellDelegate {\n    override func configure(cell: UICollectionViewCell) {\n        guard let swipableCell = cell as? SwipeCollectionViewCell else { return }\n        swipableCell.delegate = self\n    }\n    \n    func collectionView(_ collectionView: UICollectionView, editActionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -\u003e [SwipeAction]? {\n        guard orientation == .right else { return nil }\n        \n        guard let deletable = component(at: indexPath, to: Deletable.self) else { return nil }\n        \n        let deleteAction = SwipeAction(style: .destructive, title: \"Delete\") { action, indexPath in\n            deletable.delete()\n            action.fulfill(with: .delete)\n        }\n        \n        deleteAction.image = UIImage(systemName: \"trash\")\n        return [deleteAction]\n    }\n}\n```\n\nTodoDataSource is inherited from DiffableDataSource and customize it to use SwipeCellKit for swipe actions.\n\n\n## Iterable Data\n\nListKit provides `render(of: [T])`, `HGroup(of: [T])` and `VGroup(of: [T])` to handle iterable data and define dynamic layout with that data.\n\n```swift\nclass ComplexLayoutViewController: BaseViewController {\n    \n    let colors: [UIColor] = [\n        UIColor.red,\n        UIColor.orange,\n        UIColor.yellow,\n        UIColor.green,\n        UIColor.blue,\n        UIColor.brown,\n        UIColor.purple,\n        UIColor.systemPink,\n        UIColor.magenta\n    ]\n    \n    var randomColor: UIColor {\n        return colors.randomElement() ?? .cyan\n    }\n    \n    override func render() {\n        renderer.render(of: Array(0..\u003c10)) { index in\n            Section(id: index) {\n                HGroup(width: .fractionalWidth(1.0), height: .absolute(150)) {\n                    for _ in 0..\u003c3 {\n                        ColorBox2Component(color: randomColor, width: .fractionalWidth(0.5), height: .fractionalHeight(1.0))\n                        VGroup(of: [0, 1], width: .fractionalWidth(0.25), height: .fractionalHeight(1.0)) { _ in\n                            ColorBox2Component(color: randomColor, width: .fractionalWidth(1.0), height: .fractionalHeight(0.5))\n                        }\n                        VGroup(of: [0, 1], width: .fractionalWidth(0.25), height: .fractionalHeight(1.0)) { _ in\n                            ColorBox2Component(color: randomColor, width: .fractionalWidth(1.0), height: .fractionalHeight(0.5))\n                        }\n                    }\n                }\n            }\n            .orthogonalScrollingBehavior(.groupPaging)\n            .boundarySupplementaryItem(SectionHeaderComponent(title: \"Section \\(index + 1)\"))\n        }\n    }\n}\n```\n\n## Installation\n\nListKit only support Swift Package Manager. \n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/ReactComponentKit/ListKit.git\", from: \"1.1.1\"),\n]\n```\n\n## Requirements\n\n- Swift 5.4+\n  - ListKit uses @resultBuilder which is available after Swift 5.4.\n- iOS 13.0+\n- Xcode 12.+\n\n## Inspired by and Respect\n\n- [Carbon](https://github.com/ra1028/Carbon)\n  - Carbon is the awesome library for building user interfaces in UITableView and UICollectionView. It provides declarative and component-based way to buiild UI much like SwiftUI. I learned many things from Carbon to make ListKt. I want to give huge thanks to Carbon and respect it.\n\n## API reference\n\n### Section\n- `Section(id: Hashable) { // building a group }`\n  -  Section's initializer\n- `func orthogonalScrollingBehavior(_ value: UICollectionLayoutSectionOrthogonalScrollingBehavior) -\u003e Section`\n  - The section's scrolling behavior in relation to the main layout axis.\n- `func interGroupSpacing(_ value: CGFloat) -\u003e Section`\n  - The amount of space between the groups in the section.\n- `func contentInsets(top: CGFloat = 0, leading: CGFloat = 0, bottom: CGFloat = 0, trailing: CGFloat = 0) -\u003e Section`\n  - The amount of space between the content of the section and its boundaries.\n- `func contentInsetsReference(_ value: UIContentInsetsReference) -\u003e Section`\n  - @available(iOS 14.0, *)\n  - The boundary to reference when defining content insets.\n- `func supplementariesFollowContentInsets(_ value: Bool) -\u003e Section`\n  - A Boolean value that indicates whether the section's supplementary items follow the specified content insets for the section.\n- `func boundarySupplementaryItem\u003cS: SupplementaryComponent\u003e(_ value: S) -\u003e Section`\n  - A supplementary item that it associated with the boundary edges of the section, such as headers and footers.\n  - You can set multiple supplementary items with the chain of function calls.\n- `func decorationItem\u003cD: DecorationComponent\u003e(_ value: D) -\u003e Section`\n  - A decoration item that it anchored to the section, such as background decoration views.\n  - You can set multiple supplementary items with the chain of function calls.\n- `func visibleItemsInvalidationHandler(_ value: NSCollectionLayoutSectionVisibleItemsInvalidationHandler?) -\u003e Section`\n  - A closure called before each layout cycle to allow modification of the items in the section immediately before they are displayed.\n\n  \n### Group(HGroup and VGroup)\n\n- `[V|H]Group(width: NSCollectionLayoutDimension, height: NSCollectionLayoutDimension) { // builing items }`\n- `[V|H]Group(of items: [T], width: NSCollectionLayoutDimension, height: NSCollectionLayoutDimension) { // builing items }`\n  - Group's initializer\n- `func interItemSpacing(_ value: NSCollectionLayoutSpacing) -\u003e Group`\n  - The amount of space between the items in the group.\n- `func supplementaryItem\u003cS: SupplementaryComponent\u003e(_ value: S) -\u003e Group`\n  - The supplementary item that is anchored to the group.\n- `func edgeSpacing(top: NSCollectionLayoutSpacing = .fixed(0), leading: NSCollectionLayoutSpacing = .fixed(0), bottom: NSCollectionLayoutSpacing = .fixed(0), trailing: NSCollectionLayoutSpacing = .fixed(0)) -\u003e Group`\n  - The amount of space added around the boundaries of the item between other items and this item's container.\n- `func contentInsets(top: CGFloat = 0, leading: CGFloat = 0, bottom: CGFloat = 0, trailing: CGFloat = 0) -\u003e Group`\n  - The amount of space added around the content of the item to adjust its final size after its position is computed.\n\n\n### Component\n\n- `var id: AnyHashable { get }`\n  - Component's unique ID\n- `func contentView() -\u003e Content`\n  - return component's content view instance\n- `func layoutSize() -\u003e NSCollectionLayoutSize`\n  - The component's size expressed in width and height dimensions.\n- `func edgeSpacing() -\u003e NSCollectionLayoutEdgeSpacing?`\n  - The amount of space added around the boundaries of the item between other components and this component's container.\n- `func contentInsets() -\u003e NSDirectionalEdgeInsets`\n  - The amount of space added around the content of the component to adjust its final size after its position is computed.\n- `func supplementComponents() -\u003e [AnySupplementaryComponent]`\n  - An array of the supplementary items attached to the component. \n- `func willDisplay(content: Content)`\n  - Component's content is about to be displayed in the collection view.\n- `func didEndDisplay(content: Content)`\n  - Component's content was removed from the collection view.\n- `func render(in content: Content)`\n  - Render data to component's content view \n\n  \n### DataSource\n\n- `func configure(cell: UICollectionViewCell)`\n  - Configure UICollectionViewCell if needed\n- `func component\u003cT\u003e(at indexPath: IndexPath, to: T.Type) -\u003e T?`\n  - Query the component at IndexPath and casting it to T type if there is. \n\n  \n## MIT License\n\nMIT License\n\nCopyright (c) 2021 ListKit\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactcomponentkit%2Flistkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freactcomponentkit%2Flistkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactcomponentkit%2Flistkit/lists"}