/
IdentifiedArray.swift
302 lines (261 loc) · 9.29 KB
/
IdentifiedArray.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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
import Foundation
/// An array of elements that can be identified by a given key path.
///`
/// A useful container of state that is intended to interface with SwiftUI/`ForEach`. For example,
/// your application may model a counter in an identifiable fashion:
///
/// ```swift
/// struct CounterState: Identifiable {
/// let id: UUID
/// var count = 0
/// }
/// enum CounterAction { case incr, decr }
/// let counterReducer = Reducer<CounterState, CounterAction, Void> { ... }
/// ```
///
/// This domain can be pulled back to a larger domain with the ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-90ox5`` method:
///
/// ```swift
/// struct AppState { var counters = IdentifiedArrayOf<CounterState>() }
/// enum AppAction { case counter(id: UUID, action: CounterAction) }
/// let appReducer = counterReducer.forEach(
/// state: \AppState.counters,
/// action: /AppAction.counter(id:action:),
/// environment: { $0 }
/// )
/// ```
///
/// And then SwiftUI can work with this array of identified elements in a list view:
///
/// ```swift
/// struct AppView: View {
/// let store: Store<AppState, AppAction>
///
/// var body: some View {
/// List {
/// ForEachStore(
/// self.store.scope(state: \.counters, action: AppAction.counter(id:action:)),
/// content: CounterView.init(store:)
/// )
/// }
/// }
/// }
/// ```
///
public struct IdentifiedArray<ID, Element>: MutableCollection, RandomAccessCollection
where ID: Hashable {
/// A key path to a value that identifies an element.
public let id: KeyPath<Element, ID>
/// A raw array of each element's identifier.
public private(set) var ids: [ID]
/// A raw array of the underlying elements.
public var elements: [Element] { Array(self) }
// TODO: Support multiple elements with the same identifier but different data.
private var dictionary: [ID: Element]
/// Initializes an identified array with a sequence of elements and a key
/// path to an element's identifier.
///
/// - Parameters:
/// - elements: A sequence of elements.
/// - id: A key path to a value that identifies an element.
public init<S>(_ elements: S, id: KeyPath<Element, ID>) where S: Sequence, S.Element == Element {
self.id = id
let idsAndElements = elements.map { (id: $0[keyPath: id], element: $0) }
self.ids = idsAndElements.map { $0.id }
self.dictionary = Dictionary(idsAndElements, uniquingKeysWith: { $1 })
}
/// Initializes an empty identified array with a key path to an element's
/// identifier.
///
/// - Parameter id: A key path to a value that identifies an element.
public init(id: KeyPath<Element, ID>) {
self.init([], id: id)
}
public var startIndex: Int { self.ids.startIndex }
public var endIndex: Int { self.ids.endIndex }
public func index(after i: Int) -> Int {
self.ids.index(after: i)
}
public func index(before i: Int) -> Int {
self.ids.index(before: i)
}
public subscript(position: Int) -> Element {
// NB: `_read` crashes Xcode Preview compilation.
get { self.dictionary[self.ids[position]]! }
_modify { yield &self.dictionary[self.ids[position]]! }
}
#if DEBUG
/// Direct access to an element by its identifier.
///
/// - Parameter id: The identifier of element to access. Must be a valid identifier for an
/// element of the array and will _not_ insert elements that are not already in the array, or
/// remove elements when passed `nil`. Use `append` or `insert(_:at:)` to insert elements. Use
/// `remove(id:)` to remove an element by its identifier.
/// - Returns: The element.
public subscript(id id: ID) -> Element? {
get { self.dictionary[id] }
set {
if newValue != nil && self.dictionary[id] == nil {
fatalError(
"""
Can't update element with identifier \(id) because no such element exists in the array.
If you are trying to insert an element into the array, use the "append" or "insert" \
methods.
"""
)
}
if newValue == nil {
fatalError(
"""
Can't update element with identifier \(id) with nil.
If you are trying to remove an element from the array, use the "remove(id:) method."
"""
)
}
if newValue![keyPath: self.id] != id {
fatalError(
"""
Can't update element at identifier \(id) with element having mismatched identifier \
\(newValue![keyPath: self.id]).
If you would like to replace the element with identifier \(id) with an element with a \
new identifier, remove the existing element and then insert the new element, instead.
"""
)
}
self.dictionary[id] = newValue
}
}
#else
public subscript(id id: ID) -> Element? {
// NB: `_read` crashes Xcode Preview compilation.
get { self.dictionary[id] }
_modify { yield &self.dictionary[id] }
}
#endif
public mutating func insert(_ newElement: Element, at i: Int) {
let id = newElement[keyPath: self.id]
self.dictionary[id] = newElement
self.ids.insert(id, at: i)
}
public mutating func insert<C>(
contentsOf newElements: C, at i: Int
) where C: Collection, Element == C.Element {
for newElement in newElements.reversed() {
self.insert(newElement, at: i)
}
}
/// Removes and returns the element with the specified identifier.
///
/// - Parameter id: The identifier of the element to remove.
/// - Returns: The removed element.
@discardableResult
public mutating func remove(id: ID) -> Element {
let element = self.dictionary[id]
assert(element != nil, "Unexpectedly found nil while removing an identified element.")
self.dictionary[id] = nil
self.ids.removeAll(where: { $0 == id })
return element!
}
@discardableResult
public mutating func remove(at position: Int) -> Element {
self.remove(id: self.ids.remove(at: position))
}
public mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows {
var ids: [ID] = []
for (index, id) in zip(self.ids.indices, self.ids).reversed() {
if try shouldBeRemoved(self.dictionary[id]!) {
self.ids.remove(at: index)
ids.append(id)
}
}
for id in ids where !self.ids.contains(id) {
self.dictionary[id] = nil
}
}
public mutating func remove(atOffsets offsets: IndexSet) {
for offset in offsets.reversed() {
_ = self.remove(at: offset)
}
}
public mutating func move(fromOffsets source: IndexSet, toOffset destination: Int) {
self.ids.move(fromOffsets: source, toOffset: destination)
}
public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows {
try self.ids.sort {
try areInIncreasingOrder(self.dictionary[$0]!, self.dictionary[$1]!)
}
}
public mutating func shuffle<T>(using generator: inout T) where T: RandomNumberGenerator {
ids.shuffle(using: &generator)
}
public mutating func shuffle() {
var rng = SystemRandomNumberGenerator()
self.shuffle(using: &rng)
}
public mutating func reverse() {
ids.reverse()
}
}
extension IdentifiedArray: CustomDebugStringConvertible {
public var debugDescription: String {
self.elements.debugDescription
}
}
extension IdentifiedArray: CustomReflectable {
public var customMirror: Mirror {
Mirror(reflecting: self.elements)
}
}
extension IdentifiedArray: CustomStringConvertible {
public var description: String {
self.elements.description
}
}
extension IdentifiedArray: Decodable where Element: Decodable & Identifiable, ID == Element.ID {
public init(from decoder: Decoder) throws {
self.init(try [Element](from: decoder))
}
}
extension IdentifiedArray: Encodable where Element: Encodable {
public func encode(to encoder: Encoder) throws {
try self.elements.encode(to: encoder)
}
}
extension IdentifiedArray: Equatable where Element: Equatable {}
extension IdentifiedArray: Hashable where Element: Hashable {}
extension IdentifiedArray where Element: Comparable {
public mutating func sort() {
sort(by: <)
}
}
extension IdentifiedArray: ExpressibleByArrayLiteral where Element: Identifiable, ID == Element.ID {
public init(arrayLiteral elements: Element...) {
self.init(elements)
}
}
extension IdentifiedArray where Element: Identifiable, ID == Element.ID {
public init<S>(_ elements: S) where S: Sequence, S.Element == Element {
self.init(elements, id: \.id)
}
}
extension IdentifiedArray: RangeReplaceableCollection
where Element: Identifiable, ID == Element.ID {
public init() {
self.init([], id: \.id)
}
public mutating func replaceSubrange<C, R>(_ subrange: R, with newElements: C)
where C: Collection, R: RangeExpression, Element == C.Element, Index == R.Bound {
let replacingIds = self.ids[subrange]
let newIds = newElements.map { $0.id }
ids.replaceSubrange(subrange, with: newIds)
for element in newElements {
self.dictionary[element.id] = element
}
for id in replacingIds where !self.ids.contains(id) {
self.dictionary[id] = nil
}
}
}
/// A convenience type to specify an `IdentifiedArray` by an identifiable element.
public typealias IdentifiedArrayOf<Element> = IdentifiedArray<Element.ID, Element>
where Element: Identifiable