-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathComponentModel.swift
More file actions
285 lines (257 loc) · 10.6 KB
/
Copy pathComponentModel.swift
File metadata and controls
285 lines (257 loc) · 10.6 KB
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
#if os(OSX)
import Foundation
#else
import UIKit
#endif
/// The ComponentModel struct is used to configure a Component object
public struct ComponentModel: Codable, Equatable {
/// An enum with all the string keys used in the view model
public enum Key: String, CodingKey {
case identifier
case index
case kind
case header
case interaction
case footer
case model
case layout
case items
case size
case meta
case amountOfItemsToCache
}
/// Identifier
public var identifier: String?
/// The index of the Item when appearing in a list, should be computed and continuously updated by the data source
public var index: Int = 0
/// Determines which component that should be used.
/// Default kinds are: list, grid and carousel
public var kind: ComponentKind = .list
/// The header identifier
public var header: Item?
/// User interaction properties
public var interaction: Interaction
/// The footer identifier
public var footer: Item?
/// Layout properties
public var layout: Layout
/// A collection of view models
public var items: [Item] = [Item]()
/// The width and height of the component, usually calculated and updated by the UI component
public var size: CGSize = .zero
/// A key-value dictionary for any additional information
public var meta = [String: Any]()
/// An optional Int that is used to limit the amount of items that should be transformed into JSON
public var amountOfItemsToCache: Int?
/// An optional Codable model
var model: ComponentModelCodable?
/// Initializes a component and configures it with the provided parameters
///
/// - parameter identifier: A optional string.
/// - parameter header: Determines which header item that should be used for the model.
/// - parameter kind: The type of ComponentModel that should be used.
/// - parameter layout: Configures the layout properties for the model.
/// - parameter interaction: Configures the interaction properties for the model.
/// - parameter span: Configures the layout span for the model.
/// - parameter items: A collection of view models
///
/// - returns: An initialized component
public init(identifier: String? = nil,
header: Item? = nil,
footer: Item? = nil,
kind: ComponentKind = Configuration.shared.defaultComponentKind,
layout: Layout = Layout(),
interaction: Interaction = .init(),
items: [Item] = [],
meta: [String: Any] = [:]) {
self.identifier = identifier
self.kind = kind
self.layout = layout
self.interaction = interaction
self.header = header
self.footer = footer
self.items = items.refreshIndexes()
self.meta = meta
}
/// Initializes a component and configures it with the provided parameters
///
/// - parameter identifier: A optional string.
/// - parameter header: Determines which header item that should be used for the model.
/// - parameter kind: The type of ComponentModel that should be used.
/// - parameter layout: Configures the layout properties for the model.
/// - parameter interaction: Configures the interaction properties for the model.
/// - parameter span: Configures the layout span for the model.
/// - parameter model: A generic component model that conforms to `ComponentSubModel`.
/// - parameter items: A collection of view models
///
/// - returns: An initialized component
public init<T: ComponentSubModel>(identifier: String? = nil,
header: Item? = nil,
footer: Item? = nil,
kind: ComponentKind = Configuration.shared.defaultComponentKind,
layout: Layout = Layout(),
interaction: Interaction = .init(),
model: T? = nil,
items: [Item] = [],
meta: [String: Any] = [:]) {
self.init(identifier: identifier,
header: header,
footer: footer,
kind: kind,
layout: layout,
interaction: interaction,
items: items,
meta: meta)
self.model = model
}
// MARK: - Codable
/// Initialize with a decoder.
///
/// - Parameter decoder: A decoder that can decode values into in-memory representations.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
self.identifier = try container.decodeIfPresent(String.self, forKey: .identifier)
self.index = try container.decodeIfPresent(Int.self, forKey: .index) ?? 0
self.kind = try container.decodeIfPresent(ComponentKind.self, forKey: .kind) ?? .list
self.header = try container.decodeIfPresent(Item.self, forKey: .header)
self.interaction = try container.decodeIfPresent(Interaction.self,
forKey: .interaction) ?? Interaction()
self.footer = try container.decodeIfPresent(Item.self, forKey: .footer)
self.layout = try container.decodeIfPresent(Layout.self, forKey: .layout) ?? Layout()
self.model = try container.decodeIfPresentWithModelCoder(forKey: .model)
self.items = try container.decodeIfPresent([Item].self, forKey: .items)?.refreshIndexes() ?? []
self.size = try container.decodeIfPresent(Size.self, forKey: .size)?.cgSize ?? .zero
self.meta = container.decodeJsonDictionaryIfPresent(forKey: .meta) ?? [:]
self.amountOfItemsToCache = try container.decodeIfPresent(Int.self, forKey: .amountOfItemsToCache)
}
/// Encode the struct into data.
///
/// - Parameter encoder: An encoder that can encode the struct into data.
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Key.self)
try container.encodeIfPresent(identifier, forKey: .identifier)
try container.encode(index, forKey: .index)
try container.encode(kind, forKey: .kind)
try container.encodeIfPresent(header, forKey: .header)
try container.encode(interaction, forKey: .interaction)
try container.encodeIfPresent(footer, forKey: .footer)
try container.encode(layout, forKey: .layout)
let itemsToCache: [Item]
if let amountOfItems = amountOfItemsToCache {
itemsToCache = Array(items[0..<min(amountOfItems, items.count)])
} else {
itemsToCache = items
}
try container.encodeIfPresent(itemsToCache, forKey: .items)
try container.encodeIfPresent(Size(cgSize: size), forKey: .size)
container.encode(jsonDictionary: meta, forKey: .meta)
try container.encodeIfPresent(amountOfItemsToCache, forKey: .amountOfItemsToCache)
try container.encodeIfPresentWithModel(model, forKey: .model)
}
// MARK: - Helpers
/// A generic convenience method for resolving meta attributes
///
/// - Parameter key: String
/// - Parameter defaultValue: A generic value that works as a fallback if the key value object cannot be cast into the generic type
///
/// - Returns: A generic value based on `defaultValue`, it falls back to `defaultValue` if type casting fails
public func meta<T>(_ key: String, _ defaultValue: T) -> T {
return meta[key] as? T ?? defaultValue
}
/// A convenience method for resolving meta attributes for CGFloats.
///
/// - Parameter key: String.
/// - Parameter defaultValue: A CGFloat value to be used as default if meta key is not found.
///
/// - Returns: A generic value based on `defaultValue`, it falls back to `defaultValue` if type casting fails
public func meta(_ key: String, _ defaultValue: CGFloat) -> CGFloat {
if let doubleValue = meta[key] as? Double {
return CGFloat(doubleValue)
} else if let intValue = meta[key] as? Int {
return CGFloat(intValue)
}
return defaultValue
}
/// A generic convenience method for resolving meta attributes
///
/// - parameter key: String
/// - parameter type: A generic type used for casting the meta property to a specific value or reference type
/// - returns: An optional generic value based on `type`
public func meta<T>(_ key: String, type: T.Type) -> T? {
return meta[key] as? T
}
///Compare two components
///
/// - parameter component: A ComponentModel used for comparison
///
/// - returns: A ComponentModelDiff value, see ComponentModelDiff for values.
public func diff(model: ComponentModel) -> ComponentModelDiff {
// Determine if the UI component is the same, used when Controller needs to replace the entire UI component
if kind != model.kind {
return .kind
}
// Determine if the unqiue identifier for the component changed
if identifier != model.identifier {
return .identifier
}
// Determine if the component layout changed, this can be used to trigger layout related processes
if layout != model.layout {
return .layout
}
// Determine if the header for the component has changed
if !optionalCompare(lhs: header, rhs: model.header) {
return .header
}
// Determine if the header for the component has changed
if !optionalCompare(lhs: footer, rhs: model.footer) {
return .footer
}
// Check if meta data for the component changed, this can be up to the developer to decide what course of action to take.
if !(meta as NSDictionary).isEqual(to: model.meta) {
return .meta
}
var modelsAreEqual: Bool = true
if let lhsModel = self.model {
if let rhsModel = model.model {
modelsAreEqual = lhsModel.equal(to: rhsModel)
} else {
modelsAreEqual = false
}
}
if !modelsAreEqual {
return .model
}
// Check if the items have changed
if !(items === model.items) {
return .items
}
return .none
}
/// A generic convenience method for resolving a model
///
/// - Returns: A generic model based on `type`
public func resolveModel<T: ComponentSubModel>() -> T? {
return model as? T
}
/// A generic convenience method for updating a model
///
/// - Parameter model: The model that should be updated.
public mutating func update<T: ComponentSubModel>(model: T) {
self.model = model
}
/// Add layout to component.
///
/// - Parameter layout: A layout model.
mutating public func add(layout: Layout) {
self.layout = layout
}
/// Perform mutations on the existing layout
///
/// - Parameter layout: The layout that will be used on the component model.
/// - Returns: The component that was mutated.
mutating public func configure(with layout: Layout) -> ComponentModel {
var copy = self
copy.layout = layout
return copy
}
}