-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathCellViewModel.swift
219 lines (179 loc) · 7.17 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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
//
// 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?)
/// Tells the view model that its cell is about to be displayed in the collection view.
/// This corresponds to the delegate method `collectionView(_:willDisplay:forItemAt:)`.
func willDisplay()
/// Tells the view model that its cell was removed from the collection view.
/// This corresponds to the delegate method `collectionView(_:didEndDisplaying:forItemAt:)`.
func didEndDisplaying()
}
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)
}
/// Default implementation. Does nothing.
public func willDisplay() { }
/// Default implementation. Does nothing.
public func didEndDisplaying() { }
}
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:
public func willDisplay() {
self._willDisplay()
}
/// :nodoc:
public func didEndDisplaying() {
self._didEndDisplaying()
}
/// :nodoc: "override" the extension
public let cellClass: AnyClass
/// :nodoc: "override" the 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
private let _willDisplay: () -> Void
private let _didEndDisplaying: () -> 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._willDisplay = viewModel.willDisplay
self._didEndDisplaying = viewModel.didEndDisplaying
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)"
}
}
}