Add DocC (#2)

This commit is contained in:
siyuyue 2023-01-16 10:32:06 -08:00 committed by GitHub
parent e92d676416
commit dae3d67e9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 154 additions and 16 deletions

5
.spi.yml Normal file
View File

@ -0,0 +1,5 @@
version: 1
builder:
configs:
- platform: ios
documentation_targets: [ListDiffUI]

View File

@ -1,5 +1,7 @@
import UIKit
/// Cell class that extends from UICollectionViewCell.
///
open class ListCell: UICollectionViewCell {
class var reuseIdentifier: String {
@ -13,6 +15,10 @@ open class ListCell: UICollectionViewCell {
controller?.cellDidLayoutSubviews()
}
/// Disallow overriding `prepareForReuse()`.
///
/// For cell reuse logic, it should be handled in ``ListCellController``'s life cycle methods,
/// e.g., ``ListCellController/didMount(onCell:)``, ``ListCellController/willUnmount(onCell:)``.
public override func prepareForReuse() {
}
}

View File

@ -1,5 +1,7 @@
import UIKit
/// Extend this protocol from ListCellController subclass and provide the associatedtype to use the delegate pattern.
/// The same delegate passed in from ``ListSection`` will be available to the ListCellController as the `delegate` property.
public protocol ListCellControllerWithDelegate {
associatedtype DelegateType
var delegate: DelegateType? { get }
@ -12,6 +14,7 @@ extension ListCellControllerWithDelegate where Self: AnyListCellController {
}
}
/// Used to wrap AnyObject with a weak reference.
public final class WeakAnyObject {
public static var none = WeakAnyObject(object: nil)
@ -28,6 +31,7 @@ public protocol ListCellControllerViewModelProviding {
associatedtype ListViewModelType: ListViewModel
}
/// Generic cell controller class.
open class ListCellController<
ListViewModelType: ListViewModel & Equatable,
ListViewStateType: ListViewState,
@ -45,6 +49,7 @@ open class ListCellController<
private var updatingViewModel = false
private var didLayoutSubiewsWhenInvalidatingLayout = false
/// Designated initializer.
required public init(
viewModel: ListViewModel,
delegate: WeakAnyObject,
@ -56,6 +61,10 @@ open class ListCellController<
super.init(viewModel: viewModel, delegate: delegate, context: context)
}
/// Used to update ViewState.
///
/// Calling it will trigger ``ListCellController/configureCell(cell:)`` as long as the controller has a mounted cell.
///
public func updateState(_ viewState: ListViewStateType) {
self.viewState = viewState
if !updatingViewModel {
@ -63,36 +72,60 @@ open class ListCellController<
}
}
/// Subclassing point. Provide cell size based on the container size (collection view size inset by content edge insets).
///
/// If the collection view can change size, you need to implement proper layout invalidation logic in the flow layout object,
/// in order to make sure this method is invoked to retrieve the updated cell size.
///
open override func itemSize(containerSize: CGSize) -> CGSize {
return .zero
}
/// Subclassing point. Configure cell based on ViewModel and ViewState.
///
/// It will be invoked when ViewModel or ViewState updates, and when a cell is mounted.
///
open func configureCell(cell: ListCellType) {
}
/// Subclassing point. Called when cell appears within collection view's bounds.
open func didAppear(cell: ListCellType) {
}
// cell is optional in this case since the cell might already be reused.
/// Subclassing point. Called when cell fully disappears from collection view's bounds.
///
/// Cell is optional, since the cell might already be reused.
///
open func willDisappear(cell: ListCellType?) {
}
/// Subclassing point. Called when cell fully appears within collection view's bounds.
open func didFullyAppear(cell: ListCellType) {
}
// cell is optional in this case since the cell might already be reused.
/// Subclassing point. Called when cell partially disappears from collection view's bounds.
///
/// Cell is optional, since the cell might already be reused.
///
open func willPartiallyDisappear(cell: ListCellType?) {
}
/// Subclassing point.
open func didMount(onCell cell: ListCellType) {
}
/// Subclassing point.
open func willUnmount(onCell cell: ListCellType) {
}
/// Subclassing point. Invoked when ViewModel updates.
///
/// You may perform logic that updates ViewState here.
///
open func didUpdateViewModel(oldViewModel: ListViewModelType) {
}
/// Subclassing point. Update cell layout based on ViewModel and ViewState.
open func cellDidLayoutSubviews(cell: ListCellType) {
}
@ -172,12 +205,14 @@ extension ListCellController: ListCellControllerViewModelProviding {
public typealias ListViewModelType = ListViewModelType
}
/// Type-erased base class of ``ListCellController``. Subclass ``ListCellController`` instead.
open class AnyListCellController {
class var cellType: ListCell.Type {
fatalError("Must be provided by subclass")
}
/// Used for accessing context objects passed in from ``ListDiffDataSource/init(collectionView:appleUpdatesAsync:contextObjects:)``.
public let context: ListDiffContext
weak var cell: ListCell? {

View File

@ -1,5 +1,14 @@
import Foundation
/// Used for passing in dependencies from ``ListDiffDataSource/init(collectionView:appleUpdatesAsync:contextObjects:)``
/// and available on ``AnyListCellController/context`` property.
///
/// Objects are stored in a dictonary with `ObjectIdentifier(type(of: object))` as its key/
/// Therefore you can not pass in multiple instances of the same class.
///
/// Main use case for context object is to make certain dependencies available in ``ListCellController``,
/// and not having to pass them in with ViewModels.
///
public final class ListDiffContext {
private let objects: [ObjectIdentifier: AnyObject]

View File

@ -3,22 +3,19 @@ import UIKit
private let cellAppearanceUpdateInterval: CFTimeInterval = 0.1
/// ListDiffDataSource
/// Data source object to provide data to UICollectionView.
///
/// DataSource object to provide data to UICollectionView.
/// To use ListDiffUI, create an instance of ListDiffDataSource with the corresponding UICollectionView.
/// ListDiffDataSource will sets itself as both delegate and dataSource of the UICollectionView.
/// Call ``ListDiffDataSource/setRootSection(_:animate:completion:)`` on the data source object to update the view model.
///
/// UICollectionView's layout must be an instance of UICollectionFlowLayout (or its subclass), as it implements
/// func collectionView(_: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt: IndexPath)
/// of UICollectionViewDelegateFlowLayout.
///
/// ListDiffDataSource sets itself as both delegate and dataSource of the UICollectionView. If you need to be the
/// delegate of the UICollectionView as well, set your instance as the collectionViewDelegate of the
/// ListDiffDataSource object instead.
///
/// appleUpdatesAsync: Set this to true on init to perform diffing and UI updates off main queue. Note that this will
/// cause SectionComponent's build() calls to happen off main queue as well.
public final class ListDiffDataSource: NSObject {
/// Forwards calls from the delegate of the UICollectionView.
///
/// ListDiffDataSource instance will set itself as the delegate of the collection view.
/// If you need to be the delegate of the UICollectionView as well, set your instance as the collectionViewDelegate instead.
///
public weak var collectionViewDelegate: UICollectionViewDelegate?
private let collectionView: UICollectionView
@ -34,6 +31,15 @@ public final class ListDiffDataSource: NSObject {
private var viewDataModels: [ListDiffDataModel] = []
private var viewDataModelsOnQueue: [ListDiffDataModel] = []
/// Designated initializer of ListDiffDataSource.
///
/// - Parameters:
/// - collectionView: UICollectionView's layout must be an instance of UICollectionFlowLayout (or its subclass), as it implements
/// `collectionView(_:layout:sizeForItemAt:)` of UICollectionViewDelegateFlowLayout.
/// - appleUpdatesAsync: If true, diffing will be performed asynchronously on a background thread.
/// - contextObjects: A list of objects passed in here will be available to use inside ``ListCellController``.
/// This is a way to pass in dependencies (e.g. logger) so that you don't have to piggyback them on ViewModels.
///
public init(collectionView: UICollectionView, appleUpdatesAsync: Bool = false, contextObjects: AnyObject...) {
precondition(collectionView.collectionViewLayout is UICollectionViewFlowLayout)
self.collectionView = collectionView
@ -45,6 +51,15 @@ public final class ListDiffDataSource: NSObject {
collectionView.delegate = self
}
/// Update view model with Section.
///
/// ``Section`` provides a descriptive interface to describe the structure of the collection that supports heterogenity by design.
///
/// - Parameters:
/// - section: Root section that describes the entire collection.
/// - animated: Whether diff update (insert/deletion/update) will be animated or not.
/// - completion: Completion block that gets called after diff update.
///
public func setRootSection(_ section: Section?, animate: Bool = false, completion: (() -> Void)? = nil) {
if appleUpdatesAsync {
queue.async {

View File

@ -8,6 +8,33 @@ struct ListDiffDataModel {
}
/// Base class and available for subclassing.
///
/// Section provides an intuitive interface for developers to describe how the UICollectionView should look like, that supports heterogeneity by design.
///
/// For example:
///
/// ```swift
/// CompositeSection(
/// ListSection<
/// Bool, LoadingSpinnerController
/// >(isLoading) {
/// $0 ? LoadingSpinnerViewModel() : nil
/// },
/// ListSection<
/// ItemViewModel, ItemCellController
/// >(items)
/// )
/// ```
///
/// descibes an optional loading spinner cell, and a list of items.
///
/// For its concrete implementations, ``CompositeSection`` is used for composing multiple child sections.
/// ``ListSection`` is used for declaring a list of homogeneous cells.
/// ``ListRenderSection`` is used for declaring a list of heterogenous cells.
/// It is also available for subclassing.
///
/// Note that it's ``Section/build()`` function will be called off main thread if asynchronous diffing is enabled for ``ListSectionDataSource``.
///
open class Section {
var identifier: String {
@ -20,6 +47,20 @@ open class Section {
}
/// Supports composing multiple Sections.
///
/// Example:
/// ```swift
/// CompositeSection(
/// ListSection<
/// Bool, LoadingSpinnerController
/// >(isLoading) {
/// $0 ? LoadingSpinnerViewModel() : nil
/// },
/// ListSection<
/// ItemViewModel, ItemCellController
/// >(items)
/// )
/// ```
public final class CompositeSection: Section {
private let s: [Section?]
@ -53,6 +94,13 @@ public final class ListSection<
private let transform: (T) -> ListCellControllerType.ListViewModelType?
private let delegate: WeakAnyObject
/// Initialize from a generic array of models.
///
/// - Parameters:
/// - models: Generic model array.
/// - delegate: Delegate object for ``ListCellController``.
/// - transform: A transform function that transforms from model type T to ListViewModel
/// may run on background thread if asynchronous diffing is enabled for ``ListSectionDataSource``.
public init(
_ models: [T], delegate: WeakAnyObject = .none,
transform: @escaping (T) -> ListCellControllerType.ListViewModelType?
@ -74,9 +122,9 @@ public final class ListSection<
}
}
/// Convenience init for building a single cell.
extension ListSection {
/// Convenience init for building a single cell.
public convenience init(
_ model: T, delegate: WeakAnyObject = .none,
transform: @escaping (T) -> ListCellControllerType.ListViewModelType?
@ -85,13 +133,14 @@ extension ListSection {
}
}
/// Convenience init for when input type is ListViewModelType.
extension ListSection where T == ListCellControllerType.ListViewModelType {
/// Convenience init for when input type is ListViewModelType, where transform function is omitted.
public convenience init(_ model: T, delegate: WeakAnyObject = .none) {
self.init([model], delegate: delegate) { $0 }
}
/// Convenience init for building a single cell, when input type is ListViewModelType.
public convenience init(_ models: [T], delegate: WeakAnyObject = .none) {
self.init(models, delegate: delegate) { $0 }
}
@ -103,6 +152,12 @@ public final class ListRenderSection<T: Identifiable>: Section {
private let models: [T]
private let transform: (T) -> Section?
/// Initialize from a generic array of models.
///
/// - Parameters:
/// - models: Generic model array. T must conform to ``Identifiable``.
/// - transform: A transform function that transforms from model type T to Section.
/// may run on background thread if asynchronous diffing is enabled for ``ListSectionDataSource``.
public init(_ models: [T], transform: @escaping (T) -> Section?) {
self.models = models
self.transform = transform

View File

@ -1,10 +1,19 @@
import Foundation
/// Protocol to identify ViewModel.
public protocol Identifiable {
var identifier: String { get }
}
/// ViewModel protocol that defines interface for identity and equality check.
///
/// ``Identifiable`` protocol is used to uniquely identify ViewModels in the same ``ListSection``.
/// Items in different sections are not required to have unique identifiers.
///
/// You don't need to implement its ``ListViewModel/isEqual(to:)`` function. View model should conform to Equatable protocol instead.
/// The ``EquatableNoop`` annotation is also provided to ignore a certain property from equality check.
///
public protocol ListViewModel: Identifiable {
func isEqual(to: ListViewModel) -> Bool
@ -18,6 +27,7 @@ extension ListViewModel where Self: Equatable {
}
}
/// A predefined concrete ViewModel struct to represent an empty ViewModel.
public struct ListViewModelNone: ListViewModel, Equatable {
public var identifier: String {
@ -27,6 +37,7 @@ public struct ListViewModelNone: ListViewModel, Equatable {
public static let none = ListViewModelNone()
}
/// Ignore a certain property from equality check.
@propertyWrapper public struct EquatableNoop<T>: Equatable {
public var wrappedValue: T

View File

@ -1,10 +1,12 @@
import Foundation
/// ViewState protocol.
public protocol ListViewState {
init()
}
/// A predefined concrete ViewState struct to represent an empty ViewState.
public struct ListViewStateNone: ListViewState {
public init() {