-
Notifications
You must be signed in to change notification settings - Fork 28
/
Copy pathList.swift
267 lines (228 loc) · 10.2 KB
/
List.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
//
// List.swift
// BlueprintUILists
//
// Created by Kyle Van Essen on 10/22/19.
//
import BlueprintUI
import ListableUI
import UIKit
///
/// A Blueprint element which can be used to display a Listable `ListView` within
/// an element tree.
///
/// You should use the `List` element as follows, just like you'd use the `configure(with:)` method
/// on `ListView` itself.
/// ```
/// List { list in
/// list.header = PodcastsHeader()
///
/// let podcasts = Podcast.podcasts.sorted { $0.episode < $1.episode }
///
/// list += Section("podcasts") { section in
///
/// section.header = PodcastsSectionHeader()
///
/// section += podcasts.map { podcast in
/// PodcastRow(podcast: podcast)
/// }
/// }
/// }
/// ```
/// The parameter passed to the initialization closure is an instance of `ListProperties`,
/// which holds the various configuration options and content for the list. See `ListProperties` for
/// a full overview of all the configuration options available such as animation, layout configuration, etc.
///
/// When being laid out, a `List` will take up as much space as it is allowed. If you'd like to constrain
/// the size of a list, wrap it in a `ConstrainedSize`, or other size constraining element.
///
public struct List : Element
{
/// The properties which back the on-screen list.
///
/// When it comes time to render the `List` on screen,
/// `ListView.configure(with: properties)` is called
/// to update the on-screen list with the provided properties.
public var properties : ListProperties
/// How the `List` is measured when the element is laid out
/// by Blueprint. Defaults to `.fillParent`, which means
/// it will take up all the height it is given. You can change this to
/// `.measureContent` to instead measure the optimal height.
///
/// See the `List.Measurement` documentation for more.
public var measurement : List.Measurement
//
// MARK: Initialization
//
/// Create a new list, configured with the provided properties,
/// configured with the provided `ListProperties` builder.
public init(
measurement : List.Measurement = .fillParent,
configure : ListProperties.Configure
) {
self.measurement = measurement
self.properties = .default(with: configure)
}
/// Create a new list, configured with the provided properties,
/// configured with the provided `ListProperties` builder, and the provided `sections`.
public init(
measurement : List.Measurement = .fillParent,
configure : ListProperties.Configure = { _ in },
@ListableArrayBuilder<Section> sections : () -> [Section],
@ListableValueBuilder<AnyHeaderFooterConvertible> containerHeader : () -> AnyHeaderFooterConvertible? = { nil },
@ListableValueBuilder<AnyHeaderFooterConvertible> header : () -> AnyHeaderFooterConvertible? = { nil },
@ListableValueBuilder<AnyHeaderFooterConvertible> footer : () -> AnyHeaderFooterConvertible? = { nil },
@ListableValueBuilder<AnyHeaderFooterConvertible> overscrollFooter : () -> AnyHeaderFooterConvertible? = { nil }
) {
self.measurement = measurement
var properties = ListProperties.default {
$0.sections = sections()
$0.containerHeader = containerHeader()
$0.header = header()
$0.footer = footer()
$0.overscrollFooter = overscrollFooter()
}
configure(&properties)
self.properties = properties
}
//
// MARK: Element
//
public var content : ElementContent {
ElementContent { size, env in
ListContent(
properties: self.properties,
measurement: self.measurement,
environment: env
)
}
}
public func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription? {
nil
}
}
extension List {
struct ListContent : Element {
var properties : ListProperties
var measurement : List.Measurement
init(
properties : ListProperties,
measurement : List.Measurement,
environment : Environment
) {
var properties = properties
properties.environment.blueprintEnvironment = environment
self.properties = properties
if measurement.needsMeasurement {
self.measurement = measurement
} else {
self.measurement = .fillParent
}
}
// MARK: Element
public var content : ElementContent {
switch self.measurement {
case .fillParent:
return ElementContent { constraint -> CGSize in
constraint.maximum
}
case .measureContent(let horizontalFill, let verticalFill, let safeArea, let limit):
return ElementContent() { constraint, environment -> CGSize in
let measurements = ListView.contentSize(
in: constraint.maximum,
for: self.properties,
safeAreaInsets: safeArea.safeArea(with: environment),
itemLimit: limit
)
return Self.size(
with: measurements,
in: constraint,
horizontalFill: horizontalFill,
verticalFill: verticalFill
)
}
}
}
public func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription?
{
var properties = self.properties
properties.context = properties.context ?? context.environment.listContentContext
return ListView.describe { config in
config.builder = {
ListView(frame: context.bounds, appearance: properties.appearance)
}
config.apply { listView in
listView.configure(with: properties)
}
}
}
static func size(
with size : MeasuredListSize,
in constraint : SizeConstraint,
horizontalFill : Measurement.FillRule,
verticalFill : Measurement.FillRule
) -> CGSize
{
let width : CGFloat = {
switch horizontalFill {
case .fillParent:
if let max = constraint.width.constrainedValue {
return max
} else {
fatalError(
"""
`List` is being used with the `.fillParent` measurement option, which takes \
up the full width it is afforded by its parent element. However, \
the parent element provided the `List` an unconstrained width, which is meaningless.
How do you fix this?
--------------------
1) This usually means that your `List` itself has been \
placed in a `ScrollView` or other element which intentionally provides an \
unconstrained measurement to its content. If your `List` is in a `ScrollView`, \
remove the outer scroll view – `List` manages its own scrolling. Two `ScrollViews` \
that are nested within each other is generally meaningless unless they scroll \
in different directions (eg, horizontal vs vertical).
2) If your `List` is not in a `ScrollView`, ensure that the element
measuring it is providing a constrained `SizeConstraint`.
"""
)
}
case .natural:
return size.naturalWidth ?? size.contentSize.width
}
}()
let height : CGFloat = {
switch verticalFill {
case .fillParent:
if let max = constraint.height.constrainedValue {
return max
} else {
fatalError(
"""
`List` is being used with the `.fillParent` measurement option, which takes \
up the full height it is afforded by its parent element. However, \
the parent element provided the `List` an unconstrained height, which is meaningless.
How do you fix this?
--------------------
1) This usually means that your `List` itself has been \
placed in a `ScrollView` or other element which intentionally provides an \
unconstrained measurement to its content. If your `List` is in a `ScrollView`, \
remove the outer scroll view – `List` manages its own scrolling. Two `ScrollViews` \
that are nested within each other is generally meaningless unless they scroll \
in different directions (eg, horizontal vs vertical).
2) If your `List` is not in a `ScrollView`, ensure that the element
measuring it is providing a constrained `SizeConstraint`.
"""
)
}
case .natural:
return size.contentSize.height
}
}()
return CGSize(
width: width,
height: height
)
}
}
}