Skip to content

Commit 420dc31

Browse files
authored
Merge pull request #118 from square/entin/spread-builder
Support result builder syntax for view spreading
2 parents d9f2a4c + 11446c0 commit 420dc31

File tree

3 files changed

+342
-1
lines changed

3 files changed

+342
-1
lines changed

Paralayout/UIView+Spreading.swift

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright © 2021 Square, Inc.
2+
// Copyright © 2024 Block, Inc.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -70,6 +70,35 @@ extension UIView {
7070

7171
// MARK: - Public Methods
7272

73+
#if swift(>=5.4)
74+
/// Sizes and positions subviews to equally take up all horizontal space.
75+
///
76+
/// - precondition: The available space on the horizontal axis of the receiver's bounds must be at least as large as
77+
/// the space required for the specified `margin` between each subview. In other words, the `subviews` may each have
78+
/// a size of zero along the horizontal axis, but their size may not be negative.
79+
///
80+
/// - parameter margin: The space between each subview.
81+
/// - parameter bounds: A custom area within which to layout the subviews in the receiver's coordinate space, or
82+
/// `nil` to use the receiver's `bounds`. Defaults to `nil`.
83+
/// - parameter orthogonalBehavior: Controls how the view should be sized and positioned along the vertical axis.
84+
/// Defaults to filling the vertical space of the `bounds`.
85+
/// - parameter subviews: The subviews to spread out, ordered from the leading edge to the trailing edge of the
86+
/// receiver.
87+
public func horizontallySpreadSubviews(
88+
margin: CGFloat,
89+
inRect bounds: CGRect? = nil,
90+
orthogonalBehavior: VerticalSpreadingBehavior = .fill,
91+
@ViewArrayBuilder _ subviews: () -> [UIView]
92+
) {
93+
horizontallySpreadSubviews(
94+
subviews(),
95+
margin: margin,
96+
inRect: bounds,
97+
orthogonalBehavior: orthogonalBehavior
98+
)
99+
}
100+
#endif
101+
73102
/// Sizes and positions subviews to equally take up all horizontal space.
74103
///
75104
/// - precondition: The available space on the horizontal axis of the receiver's bounds must be at least as large as
@@ -116,6 +145,34 @@ extension UIView {
116145
}
117146
}
118147

148+
#if swift(>=5.4)
149+
/// Sizes and positions subviews to equally take up all vertical space.
150+
///
151+
/// - precondition: The available space on the vertical axis of the receiver's bounds must be at least as large as
152+
/// the space required for the specified `margin` between each subview. In other words, the `subviews` may each have
153+
/// a size of zero along the vertical axis, but their size may not be negative.
154+
///
155+
/// - parameter margin: The space between each subview.
156+
/// - parameter bounds: A custom area within which to layout the subviews in the receiver's coordinate space, or
157+
/// `nil` to use the receiver's `bounds`. Defaults to `nil`.
158+
/// - parameter orthogonalBehavior: Controls how the view should be sized and positioned along the horizontal axis.
159+
/// Defaults to filling the horizontal space of the `bounds`.
160+
/// - parameter subviews: The subviews to spread out, ordered from the top edge to the bottom edge of the receiver.
161+
public func verticallySpreadSubviews(
162+
margin: CGFloat,
163+
inRect bounds: CGRect? = nil,
164+
orthogonalBehavior: HorizontalSpreadingBehavior = .fill,
165+
@ViewArrayBuilder _ subviews: () -> [UIView]
166+
) {
167+
verticallySpreadSubviews(
168+
subviews(),
169+
margin: margin,
170+
inRect: bounds,
171+
orthogonalBehavior: orthogonalBehavior
172+
)
173+
}
174+
#endif
175+
119176
/// Sizes and positions subviews to equally take up all vertical space.
120177
///
121178
/// - precondition: The available space on the vertical axis of the receiver's bounds must be at least as large as

Paralayout/ViewArrayBuilder.swift

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// Copyright © 2024 Block, 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 ViewArrayBuilder {
22+
23+
// Build expressions, which are turned into partial results.
24+
25+
public static func buildExpression(_ component: UIView) -> [UIView] {
26+
return [component]
27+
}
28+
public static func buildExpression(_ component: [UIView?]) -> [UIView] {
29+
return component.compactMap { $0 }
30+
}
31+
public static func buildExpression(_ component: [UIView]) -> [UIView] {
32+
return component
33+
}
34+
public static func buildExpression(_ component: UIView?) -> [UIView] {
35+
return [component].compactMap { $0 }
36+
}
37+
38+
// Build partial results, which accumulate.
39+
40+
public static func buildPartialBlock(first: UIView) -> [UIView] {
41+
return [first]
42+
}
43+
public static func buildPartialBlock(first: [UIView]) -> [UIView] {
44+
return first
45+
}
46+
public static func buildPartialBlock(accumulated: UIView, next: UIView) -> [UIView] {
47+
return [accumulated, next]
48+
}
49+
public static func buildPartialBlock(accumulated: UIView, next: [UIView]) -> [UIView] {
50+
return [accumulated] + next
51+
}
52+
public static func buildPartialBlock(accumulated: [UIView], next: UIView) -> [UIView] {
53+
return accumulated + [next]
54+
}
55+
public static func buildPartialBlock(accumulated: [UIView], next: [UIView]) -> [UIView] {
56+
return accumulated + next
57+
}
58+
59+
// Build if statements
60+
61+
public static func buildOptional(_ component: [UIView]?) -> [UIView] {
62+
return component ?? []
63+
}
64+
public static func buildOptional(_ component: [UIView]) -> [UIView] {
65+
return component
66+
}
67+
68+
// Build if-else and switch statements
69+
70+
public static func buildEither(first component: [UIView]) -> [UIView] {
71+
return component
72+
}
73+
public static func buildEither(second component: [UIView]) -> [UIView] {
74+
return component
75+
}
76+
77+
// Build for-loop statements
78+
79+
public static func buildArray(_ components: [[UIView]]) -> [UIView] {
80+
return components.flatMap { $0 }
81+
}
82+
83+
// Build the blocks that turn into results.
84+
85+
public static func buildBlock(_ components: [UIView]...) -> [UIView] {
86+
return components.flatMap { $0 }
87+
}
88+
89+
}
90+
#endif
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
//
2+
// Copyright © 2024 Block, 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 Paralayout
18+
import XCTest
19+
20+
#if swift(>=5.4)
21+
final class ViewArrayBuilderTests: XCTestCase {
22+
23+
// MARK: - Tests
24+
25+
func testSimpleResultBuilder() throws {
26+
let view1 = UIView()
27+
let view2 = UIView()
28+
XCTAssertEqual(
29+
viewArray {
30+
view1
31+
view2
32+
},
33+
[
34+
view1,
35+
view2,
36+
]
37+
)
38+
}
39+
40+
func testIfTrueResultBuilder() throws {
41+
let view1 = UIView()
42+
let view2 = UIView()
43+
let view3 = UIView()
44+
let condition = true
45+
XCTAssertEqual(
46+
viewArray {
47+
view1
48+
if condition {
49+
view2
50+
}
51+
view3
52+
},
53+
[
54+
view1,
55+
view2,
56+
view3,
57+
]
58+
)
59+
}
60+
61+
func testIfFalseResultBuilder() throws {
62+
let view1 = UIView()
63+
let view2 = UIView()
64+
let view3 = UIView()
65+
let condition = false
66+
XCTAssertEqual(
67+
viewArray {
68+
view1
69+
if condition {
70+
view2
71+
}
72+
view3
73+
},
74+
[
75+
view1,
76+
view3,
77+
]
78+
)
79+
}
80+
81+
func testIfElseFirstBranchResultBuilder() throws {
82+
let view1 = UIView()
83+
let view2 = UIView()
84+
let view3 = UIView()
85+
let view4 = UIView()
86+
let condition = true
87+
XCTAssertEqual(
88+
viewArray {
89+
view1
90+
if condition {
91+
view2
92+
} else {
93+
view3
94+
}
95+
view4
96+
},
97+
[
98+
view1,
99+
view2,
100+
view4,
101+
]
102+
)
103+
}
104+
105+
func testIfElseSecondBranchResultBuilder() throws {
106+
let view1 = UIView()
107+
let view2 = UIView()
108+
let view3 = UIView()
109+
let view4 = UIView()
110+
let condition = false
111+
XCTAssertEqual(
112+
viewArray {
113+
view1
114+
if condition {
115+
view2
116+
} else {
117+
view3
118+
}
119+
view4
120+
},
121+
[
122+
view1,
123+
view3,
124+
view4,
125+
]
126+
)
127+
}
128+
129+
func testSwitchCaseResultBuilder() throws {
130+
let view1 = UIView()
131+
let view2 = UIView()
132+
let view3 = UIView()
133+
let value = 1
134+
XCTAssertEqual(
135+
viewArray {
136+
view1
137+
switch value {
138+
case 1:
139+
view2
140+
default:
141+
nil
142+
}
143+
view3
144+
},
145+
[
146+
view1,
147+
view2,
148+
view3,
149+
]
150+
)
151+
}
152+
153+
func testSwitchDefaultResultBuilder() throws {
154+
let view1 = UIView()
155+
let view2 = UIView()
156+
let view3 = UIView()
157+
let value = 2
158+
XCTAssertEqual(
159+
viewArray {
160+
view1
161+
switch value {
162+
case 1:
163+
view2
164+
default:
165+
nil
166+
}
167+
view3
168+
},
169+
[
170+
view1,
171+
view3,
172+
]
173+
)
174+
}
175+
176+
func testForLoopResultBuilder() throws {
177+
let views = [UIView(), UIView(), UIView()]
178+
XCTAssertEqual(
179+
viewArray {
180+
for view in views {
181+
view
182+
}
183+
},
184+
views
185+
)
186+
}
187+
188+
// MARK: - Private Methods
189+
190+
private func viewArray(@ViewArrayBuilder _ builder: () -> [UIView]) -> [UIView] {
191+
builder()
192+
}
193+
}
194+
#endif

0 commit comments

Comments
 (0)