Code Monkey home page Code Monkey logo

compositionallist's Introduction

CompositionalList ๐Ÿงฉ

gradienta-ix_kUDzCczo-unsplash (1) ForTheBadge built-with-love Open Source? Yes! MIT license swift-version swiftui-version xcode-version swift-package-manager

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

  • 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 to SectionIdentifierViewModel. 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 to Hashable.
  • RowView the compiler will infer the return value in the CellProvider closure as long it conforms to View.
  • HeaderFooterView must conform to View, 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 of View to avoid the compiler force us to define the Types on initialization, if a header is not needed return a Spacer with a height of 0.

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 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()
      }
    }
  1. CellProvider closure that provides a model and an indexpath and expects a View as the return value. Here you can return different SwiftUI views for each section, if you use a conditional statement like a Switch in this case, you must use a Group as the return value. For example in this case the compiler will infer this as the return value:
Group<_ConditionalContent<_ConditionalContent<TileInfo, ListItem>, ArtWork>>
  1. HeaderFooterProvider closure that provides the section identifier, the kind which can be UICollectionView.elementKindSectionHeader or UICollectionView.elementKindSectionFooter this will be defined by your layout, and the indexPath for the corresponding section. It expects a View as a return value, you can customize your return value based on the section or if it's a header or a footer. Same as CellProvider if a conditional statement is used make sure to wrap it in a Group. This closure is required even If you don't define headers or footers in your layout you still need to return a View, in that case, you can return a Spacer with a height of 0. (looking for a more elegant solution by now ๐Ÿคท๐Ÿฝโ€โ™‚๏ธ).

  2. SelectionProvider closure, internally uses UICollectionViewDelegate cell did select a method to provide the selected item, this closure is optional.

  3. customLayout environment object, here you can return any kind of layout as long is a UICollectionViewLayout. You can find the code for the layout here. ๐Ÿ˜‰

  4. 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 main branch.

DEMO

k1

Important:

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.

compositionallist's People

Contributors

jamesrochabrun avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

jarakys

compositionallist's Issues

Should sync data in `updateUIViewController` method

Good library, save me tons of time to adopt CompositionalCollectionViewLayout in SwiftUI by my-self, thank you!

However I notice that the data didn't get sync into Coordinator at all, from my experience, the method makeCoordinator() usually only call once by SwiftUI, after that, even the params passed to the UIViewControllerRepresentable changed, it won't be call again.

Instead, you should sync data manually inside updateUIViewController method, making some adjustments like this in the code can solve the problem:

    public init(_ items: [ViewModel],
                @ViewBuilder cellProvider: @escaping Diff.CellProvider) {
        // This initializer will be invoked every time `items` or `cellProvider` params changed
        self.cellProvider = cellProvider
        self.itemsPerSection = items
    }

    public func makeCoordinator() -> Coordinator {
        // This method usually only call once during UIViewControllerRepresentable's lifecycle, so don't depends on it to sync latest data.
        Coordinator(self)
    }

    public func updateUIViewController(_ uiViewController: Diff, context: Context) {
        // You should add this line to sync latest params into Coordinator instance
        context.coordinator.itemsPerSection = itemsPerSection
        uiViewController.applySnapshotWith(context.coordinator.itemsPerSection)
    }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.