KarrotListKit

repository·main·Indexed 40 days ago

https://github.com/daangn/karrotlistkit

A UIKit-based listing framework that provides a declarative UI API similar to SwiftUI. It utilizes DifferenceKit for high-performance list updates and features a Component-based architecture to represent data and actions without subclassing UICollectionViewCell. Key features include the CollectionViewAdapter for bridging declarative logic to UICollectionView, customizable update strategies, pagination via onReachEnd, and support for prefetching plugins to improve scroll performance.

Tokens
2.4K
Snippets
7
Records
8
Agent score
0%

What's inside KarrotListKit

  1. How CollectionViewAdapter works

    main

    The CollectionViewAdapter acts as the bridge between UICollectionView logic and the KarrotListKit declarative logic. It encapsulates the core implementation of the framework.

    To set it up, you typically need a CollectionViewAdapterConfiguration, a UICollectionView, and a CollectionViewLayoutAdapter (which provides the sectionLayout for a UICollectionViewCompositionalLayout).

    private let configuration = CollectionViewAdapterConfiguration()
    private let layoutAdapter = CollectionViewLayoutAdapter()
    
    private lazy var collectionViewAdapter = CollectionViewAdapter(
      configuration: configuration,
      collectionView: collectionView,
      layoutAdapter: layoutAdapter
    )
    
    private lazy var collectionView = UICollectionView(
      frame: .zero,
      collectionViewLayout: UICollectionViewCompositionalLayout(
        sectionProvider: layoutAdapter.sectionLayout
      )
    )
  2. Handle Component Sizing

    main

    Components can represent their own size via an estimate using layoutMode. For precise sizing, the underlying Content (the view) should override sizeThatFits(_:).

    Additionally, SectionLayout can be customized by providing an NSCollectionLayoutSection via the .withSectionLayout modifier on a Section.

    struct ButtonComponent: Component {
      typealias Content = Button
      // ...
      var layoutMode: ContentLayoutMode {
        .flexibleHeight(estimatedHeight: 44.0)
      }
    }
    
    final class Button: UIControl {
      // ...
      override func sizeThatFits(_ size: CGSize) -> CGSize {
        // return size of a Button
      }
    }
    
    // Customizing section layout
    Section(id: "Section1") {
      // ...
    }
    .withSectionLayout { [weak self] context -> NSCollectionLayoutSection? in
      // return NSCollectionLayoutSection object
    }
  3. Present a List and update the UI

    main

    You define the UI using a declarative List structure containing Section and Cell objects. You can apply modifiers like .didSelect, .willDisplay, .withHeader, .withFooter, and .withSectionLayout to customize behavior and appearance.

    To update the UI, call collectionViewAdapter.apply(_:updateStrategy:completion:) with the new list structure.

    let list = List {
      Section(id: "Section1") {
        Cell(
          id: "Cell1",
          component: ButtonComponent(viewModel: .init(title: $0.rawValue))
        )
        Cell(
          id: "Cell2",
          component: ButtonComponent(viewModel: .init(title: $0.rawValue))
        )
        .didSelect { context in
          // handle selection
        }
        .willDisplay { context in
          // handle displaying
        }
      }
      .withHeader(ButtonComponent(viewModel: .init(title: "Header")))
      .withFooter(ButtonComponent(viewModel: .init(title: "Footer")))
      .withSectionLayout(.vertical(spacing: 12.0))
    }
    
    collectionViewAdapter.apply(
      list,
      updateStrategy: .animatedBatchUpdates  // default is animatedBatchUpdates
    ) {
      // after completion
    }
  4. Implement Pagination with onReachEnd

    main

    Use the .onReachEnd modifier on a List to trigger actions when the user scrolls near the end of the content.

    The offset parameter uses ReachedEndEvent.OffsetFromEnd to define the trigger condition:

    • relativeToContainerSize(multiplier: CGFloat): Triggers when the scroll position is within a multiple of the content view's height from the end. (Default is 2.0).
    • absolute(CGFloat): Triggers when the scroll position is within an absolute point value from the end.

    The handler is a closure that performs the asynchronous action (e.g., fetching more data).

    List(sections: [])
      .onReachEnd(
        offset: .absolute(100.0),
        handler: { _ in
          // Closure Trigger when reached end of list.
        }
      )
  5. Install KarrotListKit via Swift Package Manager

    main

    To install KarrotListKit, use Swift Package Manager in Xcode:

    1. Open Xcode and navigate to Project -> Package dependencies -> Add Package Dependency (+).
    2. Enter the repository URL: https://github.com/daangn/KarrotListKit.
    3. Specify your desired version.
    4. Click Next and Finish.

    After installation, import the framework in your Swift files:

    import KarrotListKit
  6. Use Prefetching to improve scroll performance

    main

    The CollectionViewAdapter conforms to UICollectionViewDataSourcePrefetching. You can improve performance by providing prefetchingPlugins during CollectionViewAdapter initialization.

    To support prefetching, your components should conform to relevant protocols (e.g., ComponentRemoteImagePrefetchable) and expose the necessary data (e.g., remoteImageURLs).

    let collectionViewAdapter = CollectionViewAdapter(
      configuration: .init(),
      collectionView: collectionView,
      layoutAdapter: CollectionViewLayoutAdapter(),
      prefetchingPlugins: [
        RemoteImagePrefetchingPlugin(
          remoteImagePrefetcher: RemoteImagePrefetcher()
        )
      ]
    )
    
    extension ImagePrefetchableComponent: ComponentRemoteImagePrefetchable {
      var remoteImageURLs: [URL] {
        [
          URL(string: "imageURL"),
          URL(string: "imageURL"),
          URL(string: "imageURL")
        ]
      }
    }
  7. Configure CollectionViewAdapterUpdateStrategy

    main

    The updateStrategy parameter in collectionViewAdapter.apply determines how the collection view transitions to the new state. Use the CollectionViewAdapterUpdateStrategy enum:

    • animatedBatchUpdates: (Default) Performs animated batch updates using performBatchUpdates(...). Animates insertions, deletions, and moves.
    • nonanimatedBatchUpdates: Performs batch updates without animation using UIView.performWithoutAnimation(...). More performant than reloadData() as it avoids full reconfiguration.
    • reloadData: Performs a full reloadData(). Reconfigures all visible cells; generally discouraged for performance reasons.
  8. Implement a Component

    main

    A Component is the smallest unit in the framework, allowing for a declarative representation of data and actions without needing to subclass UICollectionViewCell or UICollectionReusableView. Its interface is similar to UIViewRepresentable to ease future SwiftUI migrations.

    To implement a component, conform to the Component protocol and define:

    • Content: The type of view being rendered (e.g., UIButton, UIView).
    • ViewModel: The data type used to configure the content.
    • Coordinator: An optional type for handling actions.
    • renderContent(coordinator:): Creates the initial view.
    • render(in:coordinator:): Configures the existing view with the new view model.
    • layoutMode: Defines how the component handles sizing (e.g., .flexibleHeight).
    struct ButtonComponent: Component {
    
      typealias Content = Button
      typealias ViewModel = Button.ViewModel
      typealias Coordinator = Void
    
      let viewModel: ViewModel
    
      init(viewModel: ViewModel) {
        self.viewModel = viewModel
      }
    
      func renderContent(coordinator: Coordinator) -> Button {
        Button()
      }
    
      func render(in content: Button, coordinator: Coordinator) {
        content.configure(viewModel: viewModel)
      }
    
      var layoutMode: ContentLayoutMode {
        .flexibleHeight(estimatedHeight: 44.0)
      }
    }