-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathCellViewModel.swift
191 lines (157 loc) · 6.23 KB
/
CellViewModel.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//
// Created by Jesse Squires
// https://www.jessesquires.com
//
// Documentation
// https://jessesquires.github.io/ReactiveCollectionsKit
//
// GitHub
// https://github.com/jessesquires/ReactiveCollectionsKit
//
// Copyright © 2019-present Jesse Squires
//
import Foundation
import UIKit
/// Defines a view model that describes and configures a cell in the collection view.
@MainActor
public protocol CellViewModel: DiffableViewModel, ViewRegistrationProvider {
/// The type of cell that this view model represents and configures.
associatedtype CellType: UICollectionViewCell
/// Returns whether or not the cell should get highlighted.
/// This corresponds to the delegate method `collectionView(_:shouldHighlightItemAt:)`.
/// The default implementation returns `true`.
var shouldHighlight: Bool { get }
/// Returns a context menu configuration for the cell.
/// This corresponds to the delegate method `collectionView(_:contextMenuConfigurationForItemAt:point:)`.
var contextMenuConfiguration: UIContextMenuConfiguration? { get }
/// Configures the provided cell for display in the collection.
/// - Parameter cell: The cell to configure.
func configure(cell: CellType)
/// Handles the selection event for this cell, optionally using the provided `coordinator`.
/// - Parameter coordinator: An event coordinator object, if one was provided to the `CollectionViewDriver`.
func didSelect(with coordinator: CellEventCoordinator?)
}
extension CellViewModel {
/// Default implementation. Returns `true`.
public var shouldHighlight: Bool { true }
/// Default implementation. Returns `nil`.
public var contextMenuConfiguration: UIContextMenuConfiguration? { nil }
/// Default implementation.
/// Calls `didSelectCell(viewModel:)` on the `coordinator`,
/// passing `self` to the `viewModel` parameter.
public func didSelect(with coordinator: CellEventCoordinator?) {
coordinator?.didSelectCell(viewModel: self)
}
}
extension CellViewModel {
/// The cell class for this view model.
public var cellClass: AnyClass { CellType.self }
/// A default reuse identifier for cell registration.
/// Returns the name of the class implementing the `CellViewModel` protocol.
public var reuseIdentifier: String { "\(Self.self)" }
/// A default registration for this view model for class-based cells.
/// - Warning: Does not work for nib-based cells.
public var registration: ViewRegistration {
ViewRegistration(
reuseIdentifier: self.reuseIdentifier,
cellClass: self.cellClass
)
}
/// Returns a type-erased version of this view model.
public func eraseToAnyViewModel() -> AnyCellViewModel {
if let erasedViewModel = self as? AnyCellViewModel {
return erasedViewModel
}
return AnyCellViewModel(self)
}
// MARK: Internal
func dequeueAndConfigureCellFor(collectionView: UICollectionView, at indexPath: IndexPath) -> CellType {
let cell = self.registration.dequeueViewFor(collectionView: collectionView, at: indexPath) as! CellType
self.configure(cell: cell)
return cell
}
}
/// A type-erased cell view model.
///
/// - Note: When providing cells with mixed data types to a `SectionViewModel`,
/// it is necessary to convert them to `AnyCellViewModel`.
@MainActor
public struct AnyCellViewModel: CellViewModel {
// MARK: DiffableViewModel
/// :nodoc:
nonisolated public var id: UniqueIdentifier { self._id }
// MARK: ViewRegistrationProvider
/// :nodoc:
public var registration: ViewRegistration { self._registration }
// MARK: CellViewModel
/// :nodoc:
public typealias CellType = UICollectionViewCell
/// :nodoc:
public var shouldHighlight: Bool { self._shouldHighlight }
/// :nodoc:
public var contextMenuConfiguration: UIContextMenuConfiguration? { self._contextMenuConfiguration }
/// :nodoc:
public func configure(cell: UICollectionViewCell) {
self._configure(cell)
}
/// :nodoc:
public func didSelect(with coordinator: CellEventCoordinator?) {
self._didSelect(coordinator)
}
/// :nodoc: "override" extension
public let cellClass: AnyClass
/// :nodoc: "override" extension
public let reuseIdentifier: String
// MARK: Private
private let _viewModel: AnyHashable
private let _id: UniqueIdentifier
private let _registration: ViewRegistration
private let _shouldHighlight: Bool
private let _contextMenuConfiguration: UIContextMenuConfiguration?
private let _configure: (CellType) -> Void
private let _didSelect: (CellEventCoordinator?) -> Void
// MARK: Init
/// Initializes an `AnyCellViewModel` from the provided cell view model.
///
/// - Parameter viewModel: The view model to type-erase.
public init<T: CellViewModel>(_ viewModel: T) {
if let erasedViewModel = viewModel as? Self {
self = erasedViewModel
return
}
self._viewModel = viewModel
self._id = viewModel.id
self._registration = viewModel.registration
self._shouldHighlight = viewModel.shouldHighlight
self._contextMenuConfiguration = viewModel.contextMenuConfiguration
self._configure = { cell in
precondition(cell is T.CellType, "Cell must be of type \(T.CellType.self). Found \(cell.self)")
viewModel.configure(cell: cell as! T.CellType)
}
self._didSelect = { coordinator in
viewModel.didSelect(with: coordinator)
}
self.cellClass = viewModel.cellClass
self.reuseIdentifier = viewModel.reuseIdentifier
}
}
extension AnyCellViewModel: Equatable {
/// :nodoc:
nonisolated public static func == (left: AnyCellViewModel, right: AnyCellViewModel) -> Bool {
left._viewModel == right._viewModel
}
}
extension AnyCellViewModel: Hashable {
/// :nodoc:
nonisolated public func hash(into hasher: inout Hasher) {
self._viewModel.hash(into: &hasher)
}
}
extension AnyCellViewModel: CustomDebugStringConvertible {
/// :nodoc:
nonisolated public var debugDescription: String {
MainActor.assumeIsolated {
"\(self._viewModel)"
}
}
}