CompositionalList
CompositionalList copied to clipboard
Customizable SwiftUI List powered by UICollectionViewDiffableDataSource and UICollectionViewLayout
CompositionalList 🧩
CompositionalList is a SwiftUI UIViewControllerRepresentable wrapper powered by UIKit DiffableDataSource and Compositional Layout. 🥸
It is customizable and flexible and supports multiple sections and cell selection. It allows to use of any kind of SwiftUI view inside of cells, headers, or footers.
Requirements
- iOS 13.0 or later
Features
- [X] Supports multiple sections.
- [X] Supports adapting UI to any kind of custom layout.
- [X] Supports cell selection.
CompositionalList adds SwiftUI views as children of UICollectionViewCell's and UICollectionReusableView's using UIHostingController's, it takes an array of data structures defined by a public protocol called SectionIdentifierViewModel that holds a section identifier and an array of cell identifiers.
public protocol SectionIdentifierViewModel {
associatedtype SectionIdentifier: Hashable
associatedtype CellIdentifier: Hashable
var sectionIdentifier: SectionIdentifier { get }
var cellIdentifiers: [CellIdentifier] { get }
}
CompositionalList basic structure looks like this...
struct CompositionalList<ViewModel, RowView, HeaderFooterView> where ViewModel : SectionIdentifierViewModel, RowView : View, HeaderFooterView : View
ViewModelmust conform toSectionIdentifierViewModel. To satisfy this protocol you must create a data structure that contains a section identifier, for example, an enum, and an array of objects that conform toHashable.RowViewthe compiler will infer the return value in theCellProviderclosure as long it conforms toView.HeaderFooterViewmust conform toView, which represents a header or a footer in a section. The developer must provide a view to satisfying the generic parameter. By now we need to return any kind ofViewto avoid the compiler force us to define the Types on initialization, if a header is not needed return aSpacerwith a height of0.
Getting Started
- Read this Readme doc
- Read the How to use section.
- Clone the Example project as needed.
How to use.
CompositionalList is initialized with an array of data structures that conform to SectionIdentifierViewModel which represents a section, this means it can have one or X number of sections.
- Step 1, create a section identifier like this...
public enum SectionIdentifierExample: String, CaseIterable {
case popular = "Popular"
case new = "New"
case top = "Top Items"
case recent = "Recent"
case comingSoon = "Coming Soon"
}
- Step 2, create a data structure that conforms to
SectionIdentifierViewModel...
struct FeedSectionIdentifier: SectionIdentifierViewModel {
let sectionIdentifier: SectionIdentifierExample // <- This is your identifier for each section.
let cellIdentifiers: [FeedItemViewModel] // <- This is your model for each cell.
}
- Step 3, creating a section, can be done inside a data provider view model that conforms to
ObservableObject. 😉
For simplicity, here we are creating a single section, for the full code on how to create multiple sections check the example source code.
struct Remote: ObservableObject {
@Published var sectionIdentifiers: [FeedSectionIdentifier]
func fetch() {
/// your code for fetching some models...
sectionIdentifiers = [FeedSectionIdentifier(sectionIdentifier: .popular, cellIdentifiers: models)]
}
}
- Step4 🤖, initialize the
CompositionalListwith the array of section identifiers...
import CompositionalList
.....
@ObservedObject private var remote = Remote()
var body: some View {
NavigationView {
/// 5
if items.isEmpty {
ActivityIndicator()
} else {
CompositionalList(remote.sectionIdentifiers) { model, indexPath in
/// 1
Group {
switch indexPath.section {
case 0, 2, 3:
TileInfo(artworkViewModel: model)
case 1:
ListItem(artworkViewModel: model)
default:
ArtWork(artworkViewModel: model)
}
}
}.sectionHeader { sectionIdentifier, kind, indexPath in
/// 2
TitleHeaderView(title: sectionIdentifier?.rawValue ?? "")
}
.selectedItem {
/// 3
selectedItem = $0
}
/// 4
.customLayout(.composed())
}
}.onAppear {
remote.fetch()
}
}
CellProviderclosure that provides amodeland anindexpathand expects aViewas the return value. Here you can return differentSwiftUIviews for each section, if you use a conditional statement like aSwitchin this case, you must use aGroupas the return value. For example in this case the compiler will infer this as the return value:
Group<_ConditionalContent<_ConditionalContent<TileInfo, ListItem>, ArtWork>>
-
HeaderFooterProviderclosure that provides the section identifier, thekindwhich can beUICollectionView.elementKindSectionHeaderorUICollectionView.elementKindSectionFooterthis will be defined by your layout, and the indexPath for the corresponding section. It expects aViewas a return value, you can customize your return value based on the section or if it's a header or a footer. Same asCellProviderif a conditional statement is used make sure to wrap it in aGroup. This closure is required even If you don't define headers or footers in your layout you still need to return aView, in that case, you can return aSpacerwith a height of 0. (looking for a more elegant solution by now 🤷🏽♂️). -
SelectionProviderclosure, internally usesUICollectionViewDelegatecell did select a method to provide the selected item, this closure is optional. -
customLayoutenvironment object, here you can return any kind of layout as long is aUICollectionViewLayout. You can find the code for the layout here. 😉 -
For a reason that I still don't understand, we need to use a conditional statement verifying that the array is not empty, is handy for this case because we can return a spinner. 😬
Installation
Installation with Swift Package Manager (Xcode 11+) Swift Package Manager (SwiftPM) is a tool for managing the distribution of Swift code as well as C-family dependency. From Xcode 11, SwiftPM got natively integrated with Xcode.
CompositionalList supports SwiftPM from version 5.1.0. To use SwiftPM, you should use Xcode 11 to open your project. Click File -> Swift Packages -> Add Package Dependency, enter CompositionalList repo's URL. Or you can log in to Xcode with your GitHub account and just type CompositionalList to search.
After selecting the package, you can choose the dependency type (tagged version, branch, or commit). Then Xcode will set up all the stuff for you.
How To Collaborate
- This repo contains a convenient Compositional Layout extension to compose different layouts, feel free to add more layouts!
- Open a PR for any proposed change pointing it to
mainbranch.
DEMO

Important:
Folow the Example project 🤓
CompositionalList is open source, feel free to collaborate!
TODO:
- [ ] Improve loading data,
UIVIewRepresentabledoes not update its context, need to investigate why. - [ ] Investigate why we need to make a conditional statement checking if the data is empty inside the view.