Skip to content

Commit

Permalink
Merge pull request #118 from square/entin/spread-builder
Browse files Browse the repository at this point in the history
Support result builder syntax for view spreading
  • Loading branch information
NickEntin authored Apr 25, 2024
2 parents d9f2a4c + 11446c0 commit 420dc31
Show file tree
Hide file tree
Showing 3 changed files with 342 additions and 1 deletion.
59 changes: 58 additions & 1 deletion Paralayout/UIView+Spreading.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright © 2021 Square, Inc.
// Copyright © 2024 Block, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -70,6 +70,35 @@ extension UIView {

// MARK: - Public Methods

#if swift(>=5.4)
/// Sizes and positions subviews to equally take up all horizontal space.
///
/// - precondition: The available space on the horizontal axis of the receiver's bounds must be at least as large as
/// the space required for the specified `margin` between each subview. In other words, the `subviews` may each have
/// a size of zero along the horizontal axis, but their size may not be negative.
///
/// - parameter margin: The space between each subview.
/// - parameter bounds: A custom area within which to layout the subviews in the receiver's coordinate space, or
/// `nil` to use the receiver's `bounds`. Defaults to `nil`.
/// - parameter orthogonalBehavior: Controls how the view should be sized and positioned along the vertical axis.
/// Defaults to filling the vertical space of the `bounds`.
/// - parameter subviews: The subviews to spread out, ordered from the leading edge to the trailing edge of the
/// receiver.
public func horizontallySpreadSubviews(
margin: CGFloat,
inRect bounds: CGRect? = nil,
orthogonalBehavior: VerticalSpreadingBehavior = .fill,
@ViewArrayBuilder _ subviews: () -> [UIView]
) {
horizontallySpreadSubviews(
subviews(),
margin: margin,
inRect: bounds,
orthogonalBehavior: orthogonalBehavior
)
}
#endif

/// Sizes and positions subviews to equally take up all horizontal space.
///
/// - precondition: The available space on the horizontal axis of the receiver's bounds must be at least as large as
Expand Down Expand Up @@ -116,6 +145,34 @@ extension UIView {
}
}

#if swift(>=5.4)
/// Sizes and positions subviews to equally take up all vertical space.
///
/// - precondition: The available space on the vertical axis of the receiver's bounds must be at least as large as
/// the space required for the specified `margin` between each subview. In other words, the `subviews` may each have
/// a size of zero along the vertical axis, but their size may not be negative.
///
/// - parameter margin: The space between each subview.
/// - parameter bounds: A custom area within which to layout the subviews in the receiver's coordinate space, or
/// `nil` to use the receiver's `bounds`. Defaults to `nil`.
/// - parameter orthogonalBehavior: Controls how the view should be sized and positioned along the horizontal axis.
/// Defaults to filling the horizontal space of the `bounds`.
/// - parameter subviews: The subviews to spread out, ordered from the top edge to the bottom edge of the receiver.
public func verticallySpreadSubviews(
margin: CGFloat,
inRect bounds: CGRect? = nil,
orthogonalBehavior: HorizontalSpreadingBehavior = .fill,
@ViewArrayBuilder _ subviews: () -> [UIView]
) {
verticallySpreadSubviews(
subviews(),
margin: margin,
inRect: bounds,
orthogonalBehavior: orthogonalBehavior
)
}
#endif

/// Sizes and positions subviews to equally take up all vertical space.
///
/// - precondition: The available space on the vertical axis of the receiver's bounds must be at least as large as
Expand Down
90 changes: 90 additions & 0 deletions Paralayout/ViewArrayBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// Copyright © 2024 Block, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import UIKit

#if swift(>=5.4)
@resultBuilder
public struct ViewArrayBuilder {

// Build expressions, which are turned into partial results.

public static func buildExpression(_ component: UIView) -> [UIView] {
return [component]
}
public static func buildExpression(_ component: [UIView?]) -> [UIView] {
return component.compactMap { $0 }
}
public static func buildExpression(_ component: [UIView]) -> [UIView] {
return component
}
public static func buildExpression(_ component: UIView?) -> [UIView] {
return [component].compactMap { $0 }
}

// Build partial results, which accumulate.

public static func buildPartialBlock(first: UIView) -> [UIView] {
return [first]
}
public static func buildPartialBlock(first: [UIView]) -> [UIView] {
return first
}
public static func buildPartialBlock(accumulated: UIView, next: UIView) -> [UIView] {
return [accumulated, next]
}
public static func buildPartialBlock(accumulated: UIView, next: [UIView]) -> [UIView] {
return [accumulated] + next
}
public static func buildPartialBlock(accumulated: [UIView], next: UIView) -> [UIView] {
return accumulated + [next]
}
public static func buildPartialBlock(accumulated: [UIView], next: [UIView]) -> [UIView] {
return accumulated + next
}

// Build if statements

public static func buildOptional(_ component: [UIView]?) -> [UIView] {
return component ?? []
}
public static func buildOptional(_ component: [UIView]) -> [UIView] {
return component
}

// Build if-else and switch statements

public static func buildEither(first component: [UIView]) -> [UIView] {
return component
}
public static func buildEither(second component: [UIView]) -> [UIView] {
return component
}

// Build for-loop statements

public static func buildArray(_ components: [[UIView]]) -> [UIView] {
return components.flatMap { $0 }
}

// Build the blocks that turn into results.

public static func buildBlock(_ components: [UIView]...) -> [UIView] {
return components.flatMap { $0 }
}

}
#endif
194 changes: 194 additions & 0 deletions ParalayoutTests/ViewArrayBuilderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//
// Copyright © 2024 Block, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Paralayout
import XCTest

#if swift(>=5.4)
final class ViewArrayBuilderTests: XCTestCase {

// MARK: - Tests

func testSimpleResultBuilder() throws {
let view1 = UIView()
let view2 = UIView()
XCTAssertEqual(
viewArray {
view1
view2
},
[
view1,
view2,
]
)
}

func testIfTrueResultBuilder() throws {
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
let condition = true
XCTAssertEqual(
viewArray {
view1
if condition {
view2
}
view3
},
[
view1,
view2,
view3,
]
)
}

func testIfFalseResultBuilder() throws {
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
let condition = false
XCTAssertEqual(
viewArray {
view1
if condition {
view2
}
view3
},
[
view1,
view3,
]
)
}

func testIfElseFirstBranchResultBuilder() throws {
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
let view4 = UIView()
let condition = true
XCTAssertEqual(
viewArray {
view1
if condition {
view2
} else {
view3
}
view4
},
[
view1,
view2,
view4,
]
)
}

func testIfElseSecondBranchResultBuilder() throws {
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
let view4 = UIView()
let condition = false
XCTAssertEqual(
viewArray {
view1
if condition {
view2
} else {
view3
}
view4
},
[
view1,
view3,
view4,
]
)
}

func testSwitchCaseResultBuilder() throws {
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
let value = 1
XCTAssertEqual(
viewArray {
view1
switch value {
case 1:
view2
default:
nil
}
view3
},
[
view1,
view2,
view3,
]
)
}

func testSwitchDefaultResultBuilder() throws {
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
let value = 2
XCTAssertEqual(
viewArray {
view1
switch value {
case 1:
view2
default:
nil
}
view3
},
[
view1,
view3,
]
)
}

func testForLoopResultBuilder() throws {
let views = [UIView(), UIView(), UIView()]
XCTAssertEqual(
viewArray {
for view in views {
view
}
},
views
)
}

// MARK: - Private Methods

private func viewArray(@ViewArrayBuilder _ builder: () -> [UIView]) -> [UIView] {
builder()
}
}
#endif

0 comments on commit 420dc31

Please sign in to comment.