Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support result builder syntax for view spreading #118

Merged
merged 2 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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