Skip to content

Commit e5e3185

Browse files
authored
Create ViewDistributionBuilder (#117)
1 parent a1f0be7 commit e5e3185

File tree

3 files changed

+506
-0
lines changed

3 files changed

+506
-0
lines changed

Paralayout/UIView+Distribution.swift

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,109 @@ extension UIView {
150150
}
151151
}
152152

153+
#if swift(>=5.4)
154+
/// Arranges subviews along the vertical axis according to a distribution with fixed and/or flexible spacers.
155+
///
156+
/// * If there are no flexible elements, this will treat the distribution as vertically centered (i.e. with two
157+
/// flexible elements of equal weight at the top and bottom, respectively).
158+
/// * If there are no spacers (fixed or flexible), this will treat the distribution as equal flexible spacing
159+
/// at the top, bottom, and between each view.
160+
///
161+
/// **Examples:**
162+
///
163+
/// To stack two elements with a 10 pt margin between them:
164+
/// ```
165+
/// // This is effectively the same as [ 1.flexible, icon, 10.fixed, label, 1.flexible ].
166+
/// applyVerticalSubviewDistribution {
167+
/// icon
168+
/// 10.fixed
169+
/// label
170+
/// }
171+
/// ```
172+
///
173+
/// To evenly spread out items:
174+
/// ```
175+
/// // This is effectively the same as [ 1.flexible, button1, 1.flexible, button2, 1.flexible, button3 ].
176+
/// applyVerticalSubviewDistribution {
177+
/// button1
178+
/// button2
179+
/// button3
180+
/// }
181+
/// ```
182+
///
183+
/// To stack two elements with 50% more space below than above:
184+
/// ```
185+
/// applyVerticalSubviewDistribution {
186+
/// 2.flexible
187+
/// label
188+
/// 12.fixed
189+
/// textField
190+
/// 3.flexible
191+
/// }
192+
/// ```
193+
///
194+
/// To arrange a pair of label on the top and bottom edges of a view, with another label centered between them:
195+
/// ```
196+
/// applyVerticalSubviewDistribution {
197+
/// 8.fixed
198+
/// headerLabel
199+
/// 1.flexible
200+
/// bodyLabel
201+
/// 1.flexible
202+
/// footerLabel
203+
/// 8.fixed
204+
/// }
205+
/// ```
206+
///
207+
/// To arrange UI in a view with an interior margin:
208+
/// ```
209+
/// applyVerticalSubviewDistribution {
210+
/// icon
211+
/// 10.fixed
212+
/// label
213+
/// }, inRect: bounds.insetBy(dx: 20, dy: 40))
214+
/// ```
215+
///
216+
/// To arrange UI vertically aligned by their leading edge 10 pt in from the leading edge of their superview:
217+
/// ```
218+
/// applyVerticalSubviewDistribution {
219+
/// icon
220+
/// 1.flexible
221+
/// button
222+
/// }, orthogonalOffset: .leading(inset: 10))
223+
/// ```
224+
///
225+
/// To arrange UI vertically without simultaneously centering it horizontally (the `icon` would need independent
226+
/// horizontal positioning):
227+
/// ```
228+
/// applyVerticalSubviewDistribution {
229+
/// 1.flexible
230+
/// icon
231+
/// 2.flexible
232+
/// }, orthogonalOffset: nil)
233+
/// ```
234+
///
235+
/// - precondition: All views in the `distribution` must be subviews of the receiver.
236+
/// - precondition: The `distribution` must not include any given view more than once.
237+
///
238+
/// - parameter distribution: An array of distribution specifiers, ordered from the top edge to the bottom edge.
239+
/// - parameter layoutBounds: The region in the receiver in which to distribute the view in the receiver's
240+
/// coordinate space. Specify `nil` to use the receiver's bounds. Defaults to `nil`.
241+
/// - parameter orthogonalAlignment: The horizontal alignment to apply to the views. If `nil`, views are left in
242+
/// their horizontal position prior to the distribution. Defaults to centered with no offset.
243+
public func applyVerticalSubviewDistribution(
244+
@ViewDistributionBuilder _ distribution: () -> [ViewDistributionSpecifying],
245+
inRect layoutBounds: CGRect? = nil,
246+
orthogonalAlignment: HorizontalDistributionAlignment? = .centered(offset: 0)
247+
) {
248+
applyVerticalSubviewDistribution(
249+
distribution(),
250+
inRect: layoutBounds,
251+
orthogonalAlignment: orthogonalAlignment
252+
)
253+
}
254+
#endif
255+
153256
/// Arranges subviews along the horizontal axis according to a distribution with fixed and/or flexible spacers.
154257
///
155258
/// * If there are no flexible elements, this will treat the distribution as horizontally centered (i.e. with two
@@ -229,6 +332,110 @@ extension UIView {
229332
}
230333
}
231334

335+
#if swift(>=5.4)
336+
/// Arranges subviews along the horizontal axis according to a distribution with fixed and/or flexible spacers.
337+
///
338+
/// * If there are no flexible elements, this will treat the distribution as horizontally centered (i.e. with two
339+
/// flexible elements of equal weight at the leading and trailing edges, respectively).
340+
/// * If there are no spacers (fixed or flexible), this will treat the distribution as equal flexible spacing
341+
/// at the leading edge, trailing edge, and between each view.
342+
///
343+
/// **Examples:**
344+
///
345+
/// To stack two elements with a 10 pt margin between them:
346+
/// ```
347+
/// // This is effectively the same as [ 1.flexible, icon, 10.fixed, label, 1.flexible ].
348+
/// applyHorizontalSubviewDistribution {
349+
/// icon
350+
/// 10.fixed
351+
/// label
352+
/// }
353+
/// ```
354+
///
355+
/// To evenly spread out items:
356+
/// ```
357+
/// // This is effectively the same as [ 1.flexible, button1, 1.flexible, button2, 1.flexible, button3 ].
358+
/// applyHorizontalSubviewDistribution {
359+
/// button1
360+
/// button2
361+
/// button3
362+
/// }
363+
/// ```
364+
///
365+
/// To stack two elements with 50% more space after than before:
366+
/// ```
367+
/// applyHorizontalSubviewDistribution {
368+
/// 2.flexible
369+
/// label
370+
/// 12.fixed
371+
/// textField
372+
/// 3.flexible
373+
/// }
374+
/// ```
375+
///
376+
/// To arrange a pair of buttons on the left and right edges of a view, with a label centered between them:
377+
/// ```
378+
/// applyHorizontalSubviewDistribution {
379+
/// 8.fixed
380+
/// backButton
381+
/// 1.flexible
382+
/// titleLabel
383+
/// 1.flexible
384+
/// nextButton
385+
/// 8.fixed
386+
/// }
387+
/// ```
388+
///
389+
/// To arrange UI in a view with an interior margin:
390+
/// ```
391+
/// applyHorizontalSubviewDistribution {
392+
/// icon
393+
/// 10.fixed
394+
/// label
395+
/// }, inRect: bounds.insetBy(dx: 20, dy: 40))
396+
/// ```
397+
///
398+
/// To arrange UI horizontally aligned by their top edge 10 pt in from the top edge of their superview:
399+
/// ```
400+
/// applyHorizontalSubviewDistribution {
401+
/// icon
402+
/// 1.flexible
403+
/// button
404+
/// }, orthogonalOffset: .top(inset: 10))
405+
/// ```
406+
///
407+
/// To arrange UI horizontally without simultaneously centering it vertically (the `icon` would need independent
408+
/// vertical positioning):
409+
/// ```
410+
/// applyHorizontalSubviewDistribution {
411+
/// 1.flexible
412+
/// icon
413+
/// 2.flexible
414+
/// }, orthogonalOffset: nil)
415+
/// ```
416+
///
417+
/// - precondition: All views in the `distribution` must be subviews of the receiver.
418+
/// - precondition: The `distribution` must not include any given view more than once.
419+
///
420+
/// - parameter distribution: An array of distribution specifiers, ordered from the leading edge to the trailing
421+
/// edge.
422+
/// - parameter layoutBounds: The region in the receiver in which to distribute the view in the receiver's
423+
/// coordinate space. Specify `nil` to use the receiver's bounds. Defaults to `nil`.
424+
/// - parameter orthogonalAlignment: The vertical alignment to apply to the views. If `nil`, views are left in
425+
/// their vertical position prior to the distribution. Defaults to centered with no offset.
426+
public func applyHorizontalSubviewDistribution(
427+
@ViewDistributionBuilder _ distribution: () -> [ViewDistributionSpecifying],
428+
inRect layoutBounds: CGRect? = nil,
429+
orthogonalAlignment: VerticalDistributionAlignment? = .centered(offset: 0)
430+
) {
431+
applyHorizontalSubviewDistribution(
432+
distribution(),
433+
inRect: layoutBounds,
434+
orthogonalAlignment: orthogonalAlignment
435+
)
436+
}
437+
#endif
438+
232439
// MARK: - Private Methods
233440

234441
private func applySubviewDistribution(
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// Copyright © 2024 Square, Inc.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
//    http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import UIKit
18+
19+
#if swift(>=5.4)
20+
@resultBuilder
21+
public struct ViewDistributionBuilder {
22+
23+
// Build expressions, which are turned into partial results.
24+
25+
public static func buildExpression(_ component: ViewDistributionSpecifying) -> [ViewDistributionSpecifying] {
26+
return [component]
27+
}
28+
public static func buildExpression(_ component: [ViewDistributionSpecifying?]) -> [ViewDistributionSpecifying] {
29+
return component.compactMap { $0 }
30+
}
31+
public static func buildExpression(_ component: [ViewDistributionSpecifying]) -> [ViewDistributionSpecifying] {
32+
return component
33+
}
34+
public static func buildExpression(_ component: ViewDistributionSpecifying?) -> [ViewDistributionSpecifying] {
35+
return [component].compactMap { $0 }
36+
}
37+
38+
// Build partial results, which accumulate.
39+
40+
public static func buildPartialBlock(first: ViewDistributionSpecifying) -> [ViewDistributionSpecifying] {
41+
return [first]
42+
}
43+
public static func buildPartialBlock(first: [ViewDistributionSpecifying]) -> [ViewDistributionSpecifying] {
44+
return first
45+
}
46+
public static func buildPartialBlock(accumulated: ViewDistributionSpecifying, next: ViewDistributionSpecifying) -> [ViewDistributionSpecifying] {
47+
return [accumulated, next]
48+
}
49+
public static func buildPartialBlock(accumulated: ViewDistributionSpecifying, next: [ViewDistributionSpecifying]) -> [ViewDistributionSpecifying] {
50+
return [accumulated] + next
51+
}
52+
public static func buildPartialBlock(accumulated: [ViewDistributionSpecifying], next: ViewDistributionSpecifying) -> [ViewDistributionSpecifying] {
53+
return accumulated + [next]
54+
}
55+
public static func buildPartialBlock(accumulated: [ViewDistributionSpecifying], next: [ViewDistributionSpecifying]) -> [ViewDistributionSpecifying] {
56+
return accumulated + next
57+
}
58+
59+
// Build if statements
60+
61+
public static func buildOptional(_ component: [ViewDistributionSpecifying]?) -> [ViewDistributionSpecifying] {
62+
return component ?? []
63+
}
64+
public static func buildOptional(_ component: [ViewDistributionSpecifying]) -> [ViewDistributionSpecifying] {
65+
return component
66+
}
67+
68+
// Build if-else and switch statements
69+
70+
public static func buildEither(first component: [ViewDistributionSpecifying]) -> [ViewDistributionSpecifying] {
71+
return component
72+
}
73+
public static func buildEither(second component: [ViewDistributionSpecifying]) -> [ViewDistributionSpecifying] {
74+
return component
75+
}
76+
77+
// Build for-loop statements
78+
79+
public static func buildArray(_ components: [[ViewDistributionSpecifying]]) -> [ViewDistributionSpecifying] {
80+
return components.flatMap { $0 }
81+
}
82+
83+
// Build the blocks that turn into results.
84+
85+
public static func buildBlock(_ components: [ViewDistributionSpecifying]...) -> [ViewDistributionSpecifying] {
86+
return components.flatMap { $0 }
87+
}
88+
}
89+
#endif

0 commit comments

Comments
 (0)