diff --git a/Sources/AccessibilityFormats.swift b/Sources/AccessibilityFormats.swift index 1014443..8f3246b 100644 --- a/Sources/AccessibilityFormats.swift +++ b/Sources/AccessibilityFormats.swift @@ -16,26 +16,34 @@ import Foundation -/// Wrapper `struct` for the `accessibilityIdentifier` format that should be applied to the cells of a `UITableView` or a `UICollectionView` +// Note: The accessibility types below are not documented as they are not intended to be part +// of the `ReactiveLists` project in the long term. See https://github.com/plangrid/ReactiveLists/issues/77 + +/// :nodoc: public struct CellAccessibilityFormat: ExpressibleByStringLiteral { private let _format: String + /// :nodoc: public init(_ format: String) { self._format = format } + /// :nodoc: public init(stringLiteral value: StringLiteralType) { self._format = value } + /// :nodoc: public init(extendedGraphemeClusterLiteral value: String) { self._format = value } + /// :nodoc: public init(unicodeScalarLiteral value: String) { self._format = value } + /// :nodoc: public func accessibilityIdentifierForIndexPath(_ indexPath: IndexPath) -> String { return self._format.replacingOccurrences(of: "%{section}", with: String(indexPath.section)) .replacingOccurrences(of: "%{item}", with: String(indexPath.item)) @@ -43,26 +51,31 @@ public struct CellAccessibilityFormat: ExpressibleByStringLiteral { } } -/// Wrapper `struct` for the `accessibilityIdentifier` format that should be applied to the headers and footers of a `UITableView` or a `UICollectionView` +/// :nodoc: public struct SupplementaryAccessibilityFormat: ExpressibleByStringLiteral { private let _format: String + /// :nodoc: public init(_ format: String) { self._format = format } + /// :nodoc: public init(stringLiteral value: StringLiteralType) { self._format = value } + /// :nodoc: public init(extendedGraphemeClusterLiteral value: String) { self._format = value } + /// :nodoc: public init(unicodeScalarLiteral value: String) { self._format = value } + /// :nodoc: public func accessibilityIdentifierForSection(_ section: Int) -> String { return self._format.replacingOccurrences(of: "%{section}", with: String(section)) } diff --git a/Sources/CollectionViewDriver.swift b/Sources/CollectionViewDriver.swift index 76b8735..79205f2 100644 --- a/Sources/CollectionViewDriver.swift +++ b/Sources/CollectionViewDriver.swift @@ -193,7 +193,7 @@ extension CollectionViewDriver: UICollectionViewDataSource { /// :nodoc: public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return self.collectionViewModel?[section]?.cellViewModels?.count ?? 0 + return self.collectionViewModel?[section]?.cellViewModels.count ?? 0 } /// :nodoc: diff --git a/Sources/CollectionViewModel.swift b/Sources/CollectionViewModel.swift index fb284e7..7dff3ef 100644 --- a/Sources/CollectionViewModel.swift +++ b/Sources/CollectionViewModel.swift @@ -37,7 +37,7 @@ public protocol CollectionViewCellViewModel { /// Asks the cell model to update the `UICollectionViewCell` with the content /// in the cell model and return the updated cell. - /// - Parameter cell: the cell which contents need to be updated. + /// - Parameter cell: the cell which's content need to be updated. func applyViewModelToCell(_ cell: UICollectionViewCell) } @@ -50,9 +50,14 @@ public extension CollectionViewCellViewModel { /// View model for supplementary views in collection views. public protocol CollectionViewSupplementaryViewModel { + /// Metadata for this supplementary view. var viewInfo: SupplementaryViewInfo? { get } + /// Height of this supplementary view. var height: CGFloat? { get } + /// Asks the supplementary view model to update the `UICollectionReusableView` with the content + /// in the model and return the updated view. + /// - Parameter view: the view which's content need to be update. func applyViewModelToView(_ view: UICollectionReusableView) } @@ -89,9 +94,8 @@ public struct CollectionViewModel { /// /// - Parameter indexPath: the index path for the cell that is being retrieved public subscript(indexPath: IndexPath) -> CollectionViewCellViewModel? { - guard let section = self[indexPath.section], - let cellViewModels = section.cellViewModels, cellViewModels.count > indexPath.item else { return nil } - return cellViewModels[indexPath.item] + guard let section = self[indexPath.section], section.cellViewModels.count > indexPath.item else { return nil } + return section.cellViewModels[indexPath.item] } /// Provides a description of the collection view content in terms of diffing keys. These diffing keys @@ -105,12 +109,12 @@ public struct CollectionViewModel { } // Ensure we have a diffing key for each cell in this section - let cellDiffingKeys: [DiffingKey] = section.cellViewModels?.map { cell in + let cellDiffingKeys: [DiffingKey] = section.cellViewModels.map { cell in guard let cell = cell as? DiffableViewModel else { fatalError("When diffing is enabled you need to provide cells which are DiffableViews.") } return "\(type(of: cell))_\(cell.diffingKey)" - } ?? [] + } return (sectionDiffingKey, cellDiffingKeys) } @@ -122,8 +126,7 @@ public struct CollectionViewModel { public struct CollectionViewSectionViewModel { /// Cells to be shown in this section. - let cellViewModels: [CollectionViewCellViewModel]? - + let cellViewModels: [CollectionViewCellViewModel] /// View model for the header of this section. let headerViewModel: CollectionViewSupplementaryViewModel? @@ -143,12 +146,12 @@ public struct CollectionViewSectionViewModel { /// Initializes a collection view section view model. /// /// - Parameters: - /// - cellViewModels: the cells in this section, or `nil`. - /// - headerViewModel: the header view model, or `nil`. - /// - footerViewModel: the footer view model, or `nil`. + /// - cellViewModels: the cells in this section. + /// - headerViewModel: the header view model (defaults to `nil`). + /// - footerViewModel: the footer view model (defaults to `nil`). /// - diffingKey: the diffing key, required for automated diffing. public init( - cellViewModels: [CollectionViewCellViewModel]?, + cellViewModels: [CollectionViewCellViewModel], headerViewModel: CollectionViewSupplementaryViewModel? = nil, footerViewModel: CollectionViewSupplementaryViewModel? = nil, diffingKey: String? = nil @@ -177,7 +180,7 @@ extension CollectionViewSectionViewModel { /// :nodoc: public init( - cellViewModels: [CollectionViewCellViewModel]?, + cellViewModels: [CollectionViewCellViewModel], headerHeight: CGFloat? = nil, footerViewModel: CollectionViewSupplementaryViewModel? = nil, diffingKey: String? = nil @@ -192,7 +195,7 @@ extension CollectionViewSectionViewModel { /// :nodoc: public init( - cellViewModels: [CollectionViewCellViewModel]?, + cellViewModels: [CollectionViewCellViewModel], headerViewModel: CollectionViewSupplementaryViewModel? = nil, footerHeight: CGFloat? = nil, diffingKey: String? = nil @@ -207,7 +210,7 @@ extension CollectionViewSectionViewModel { /// :nodoc: public init( - cellViewModels: [CollectionViewCellViewModel]?, + cellViewModels: [CollectionViewCellViewModel], headerHeight: CGFloat? = nil, footerHeight: CGFloat? = nil, diffingKey: String? = nil diff --git a/Sources/Diffing.swift b/Sources/Diffing.swift index d961fb2..fd45594 100644 --- a/Sources/Diffing.swift +++ b/Sources/Diffing.swift @@ -16,8 +16,16 @@ import Foundation -/// A view that can participate in an automatic diffing algorithm. +/// A view model that can participate in an automatic diffing algorithm. public protocol DiffableViewModel { + /// The key used by the diffing algorithm to uniquely identify an element. + /// If you are using automatic diffing on a `*Driver` (which is enabled by default) + /// you are required to provide a key that uniquely identifies each element. + /// + /// Typically you want to base this diffing key on data that is stored in the model. + /// For example: + /// + /// public var diffingKey = { group.identifier } var diffingKey: DiffingKey { get } } diff --git a/Sources/SupplementaryViewInfo.swift b/Sources/SupplementaryViewInfo.swift index 15092a9..eaef61a 100644 --- a/Sources/SupplementaryViewInfo.swift +++ b/Sources/SupplementaryViewInfo.swift @@ -19,21 +19,31 @@ import UIKit /// Metadata thats required for setting up a supplementary view. public struct SupplementaryViewInfo { - + /// Stores how the view was registered (as a class or via a nib file) public let registrationMethod: ViewRegistrationMethod - + /// The reuse identifier for this supplementary view public let reuseIdentifier: String - + /// The kind of supplementary view (e.g. `header` or `footer`) public let kind: SupplementaryViewKind /// `TableViewDataSource` and `CollectionViewDataSource` will automatically apply /// an `accessibilityIdentifier` to the supplementary view based on this format. public let accessibilityFormat: SupplementaryAccessibilityFormat - public init(registrationMethod: ViewRegistrationMethod, - reuseIdentifier: String, - kind: SupplementaryViewKind, - accessibilityFormat: SupplementaryAccessibilityFormat) { + /// Initializes the metadata for a supplementary view. + /// + /// - Parameters: + /// - registrationMethod: describes how the view was registered (as a class or via a nib file) + /// - reuseIdentifier: reuse identifier for this supplementary view + /// - kind: kind of supplementary view (e.g. `header` or `footer`) + /// - accessibilityFormat: a format string that generates an accessibility identifier for + /// the view that will be mapped to this view model. + public init( + registrationMethod: ViewRegistrationMethod, + reuseIdentifier: String, + kind: SupplementaryViewKind, + accessibilityFormat: SupplementaryAccessibilityFormat + ) { self.registrationMethod = registrationMethod self.reuseIdentifier = reuseIdentifier self.kind = kind diff --git a/Sources/TableViewDriver.swift b/Sources/TableViewDriver.swift index 5aa837b..4f31ad4 100644 --- a/Sources/TableViewDriver.swift +++ b/Sources/TableViewDriver.swift @@ -64,7 +64,7 @@ open class TableViewDriver: NSObject { /// were moved/inserted/deleted. /// /// For details, see the documentation for `TableViewDriver.tableViewModel`. - private let automaticDiffingEnabled: Bool + private let _automaticDiffingEnabled: Bool private let _shouldDeselectUponSelection: Bool private var _tableViewDiffer: TableViewDiffCalculator? @@ -88,7 +88,7 @@ open class TableViewDriver: NSObject { ) { self.tableViewModel = tableViewModel self.tableView = tableView - self.automaticDiffingEnabled = automaticDiffingEnabled + self._automaticDiffingEnabled = automaticDiffingEnabled self._shouldDeselectUponSelection = shouldDeselectUponSelection super.init() tableView.dataSource = self @@ -164,7 +164,7 @@ open class TableViewDriver: NSObject { self.tableView.registerViews(for: newModel) - if self.automaticDiffingEnabled { + if self._automaticDiffingEnabled { if !self._didReceiveFirstNonNilValue { // For the first non-nil value, we want to reload data, to avoid a weird // animation where we animate in the initial state diff --git a/Sources/TableViewModel.swift b/Sources/TableViewModel.swift index 0965334..9fbe27b 100644 --- a/Sources/TableViewModel.swift +++ b/Sources/TableViewModel.swift @@ -107,7 +107,6 @@ public struct TableViewSectionViewModel { public let cellViewModels: [TableViewCellViewModel] /// View model for the header of this section. - public let headerViewModel: TableViewSectionHeaderFooterViewModel? /// View model for the footer of this section. @@ -131,6 +130,14 @@ public struct TableViewSectionViewModel { return self.cellViewModels.isEmpty } + /// Initializes a `TableViewSectionViewModel`. + /// + /// - Parameters: + /// - cellViewModels: the cell view models contained in this section. + /// - headerViewModel: a header view model for this section (defaults to `nil`). + /// - footerViewModel: a footer view model for this section (defaults to `nil`). + /// - collapsed: whether or not this section is collapsed (defaults to `false`). + /// - diffingKey: the diffing key, or `nil`. Required for automated diffing. public init( cellViewModels: [TableViewCellViewModel], headerViewModel: TableViewSectionHeaderFooterViewModel? = nil, @@ -145,6 +152,17 @@ public struct TableViewSectionViewModel { self.diffingKey = diffingKey } + /// Initializes a `TableViewSectionViewModel`. + /// + /// - Parameters: + /// - headerTitle: title for the header, or `nil`. Setting a title will cause a default header + /// to be added to this section. + /// - headerHeight: the height of the default header, if one exists. + /// - cellViewModels: the cell view models contained in this section. + /// - footerTitle: title for the footer, or `nil`. Setting a title will cause a default footer + /// to be added to this section. + /// - footerHeight: the height of the default footer, if one exists. + /// - diffingKey: the diffing key, or `nil`. Required for automated diffing. public init( headerTitle: String?, headerHeight: CGFloat?, @@ -152,7 +170,7 @@ public struct TableViewSectionViewModel { footerTitle: String? = nil, footerHeight: CGFloat? = 0, diffingKey: String? = nil - ) { + ) { self.cellViewModels = cellViewModels self.headerViewModel = PlainHeaderFooterViewModel(title: headerTitle, height: headerHeight) self.footerViewModel = PlainHeaderFooterViewModel(title: footerTitle, height: footerHeight) diff --git a/Sources/Typealiases.swift b/Sources/Typealiases.swift index 1cd4244..74e00b8 100644 --- a/Sources/Typealiases.swift +++ b/Sources/Typealiases.swift @@ -16,10 +16,17 @@ import Foundation +/// :nodoc: public typealias CommitEditingStyleClosure = (UITableViewCellEditingStyle) -> Void +/// :nodoc: public typealias DidSelectClosure = () -> Void +/// :nodoc: public typealias DidDeleteClosure = () -> Void +/// :nodoc: public typealias DidDeselectClosure = () -> Void +/// :nodoc: public typealias WillBeginEditingClosure = () -> Void +/// :nodoc: public typealias DidEndEditingClosure = () -> Void +/// :nodoc: public typealias AccessoryButtonTappedClosure = () -> Void diff --git a/Tests/CollectionView/CollectionViewDriverTests.swift b/Tests/CollectionView/CollectionViewDriverTests.swift index df89c7c..9fb47d6 100644 --- a/Tests/CollectionView/CollectionViewDriverTests.swift +++ b/Tests/CollectionView/CollectionViewDriverTests.swift @@ -32,7 +32,7 @@ final class CollectionViewDriverTests: XCTestCase { self._collectionView = TestCollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewLayout()) self._collectionViewModel = CollectionViewModel(sectionModels: [ CollectionViewSectionViewModel( - cellViewModels: nil, + cellViewModels: [], headerViewModel: TestCollectionViewSupplementaryViewModel(height: 10, viewKind: .header, sectionLabel: "A"), footerViewModel: TestCollectionViewSupplementaryViewModel(height: 11, viewKind: .footer, sectionLabel: "A")), CollectionViewSectionViewModel( @@ -44,7 +44,7 @@ final class CollectionViewDriverTests: XCTestCase { headerViewModel: TestCollectionViewSupplementaryViewModel(label: "header_C", height: 30), footerViewModel: nil), CollectionViewSectionViewModel( - cellViewModels: nil, + cellViewModels: [], headerViewModel: TestCollectionViewSupplementaryViewModel(height: nil, viewKind: .header, sectionLabel: "D"), footerViewModel: TestCollectionViewSupplementaryViewModel(height: nil, viewKind: .footer, sectionLabel: "D")), ]) @@ -209,7 +209,7 @@ final class CollectionViewDriverTests: XCTestCase { self._collectionViewDataSource.collectionViewModel = CollectionViewModel(sectionModels: [ CollectionViewSectionViewModel( - cellViewModels: nil, + cellViewModels: [], headerViewModel: TestCollectionViewSupplementaryViewModel(height: 10, viewKind: .header, sectionLabel: "X"), footerViewModel: TestCollectionViewSupplementaryViewModel(height: 11, viewKind: .footer, sectionLabel: "X")), CollectionViewSectionViewModel( diff --git a/Tests/CollectionView/CollectionViewModelTests.swift b/Tests/CollectionView/CollectionViewModelTests.swift index c10cfe9..fc2e9d0 100644 --- a/Tests/CollectionView/CollectionViewModelTests.swift +++ b/Tests/CollectionView/CollectionViewModelTests.swift @@ -29,7 +29,7 @@ final class CollectionViewModelTests: XCTestCase { footerHeight: 50 ) - XCTAssertEqual(sectionModel.cellViewModels?.count, 1) + XCTAssertEqual(sectionModel.cellViewModels.count, 1) XCTAssertEqual(sectionModel.headerViewModel?.height, 40) XCTAssertEqual(sectionModel.footerViewModel?.height, 50) XCTAssertNil(sectionModel.headerViewModel?.viewInfo) @@ -50,7 +50,7 @@ final class CollectionViewModelTests: XCTestCase { ) ) - XCTAssertEqual(sectionModel.cellViewModels?.count, 1) + XCTAssertEqual(sectionModel.cellViewModels.count, 1) XCTAssertNil(sectionModel.headerViewModel?.viewInfo) XCTAssertEqual(sectionModel.headerViewModel?.height, 40) @@ -76,7 +76,7 @@ final class CollectionViewModelTests: XCTestCase { footerHeight: 50 ) - XCTAssertEqual(sectionModel.cellViewModels?.count, 1) + XCTAssertEqual(sectionModel.cellViewModels.count, 1) XCTAssertNil(sectionModel.footerViewModel?.viewInfo) XCTAssertEqual(sectionModel.headerViewModel?.height, 40) @@ -104,7 +104,7 @@ final class CollectionViewModelTests: XCTestCase { ) ) - XCTAssertEqual(sectionModel.cellViewModels?.count, 1) + XCTAssertEqual(sectionModel.cellViewModels.count, 1) XCTAssertEqual(sectionModel.headerViewModel?.height, 40) XCTAssertEqual(sectionModel.footerViewModel?.height, 50) @@ -126,7 +126,7 @@ final class CollectionViewModelTests: XCTestCase { func testSubscripts() { let collectionViewModel = CollectionViewModel(sectionModels: [ CollectionViewSectionViewModel( - cellViewModels: nil, + cellViewModels: [], headerHeight: 42, footerHeight: nil), CollectionViewSectionViewModel(