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.
- iOS 13.0 or later
- Supports multiple sections.
- Supports adapting UI to any kind of custom layout.
- 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
ViewModel
must 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
.RowView
the compiler will infer the return value in theCellProvider
closure as long it conforms toView
.HeaderFooterView
must 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 ofView
to avoid the compiler force us to define the Types on initialization, if a header is not needed return aSpacer
with a height of0
.
- Read this Readme doc
- Read the How to use section.
- Clone the Example project as needed.
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
CompositionalList
with 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()
}
}
CellProvider
closure that provides amodel
and anindexpath
and expects aView
as the return value. Here you can return differentSwiftUI
views for each section, if you use a conditional statement like aSwitch
in this case, you must use aGroup
as the return value. For example in this case the compiler will infer this as the return value:
Group<_ConditionalContent<_ConditionalContent<TileInfo, ListItem>, ArtWork>>
-
HeaderFooterProvider
closure that provides the section identifier, thekind
which can beUICollectionView.elementKindSectionHeader
orUICollectionView.elementKindSectionFooter
this will be defined by your layout, and the indexPath for the corresponding section. It expects aView
as a return value, you can customize your return value based on the section or if it's a header or a footer. Same asCellProvider
if 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 aSpacer
with a height of 0. (looking for a more elegant solution by now 🤷🏽♂️). -
SelectionProvider
closure, internally usesUICollectionViewDelegate
cell did select a method to provide the selected item, this closure is optional. -
customLayout
environment 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 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.
- 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
main
branch.
Folow the Example project 🤓
CompositionalList is open source, feel free to collaborate!
TODO:
- Improve loading data,
UIVIewRepresentable
does 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.