From 730dd47089ad36fa79021cb40ce3d1fc88e32245 Mon Sep 17 00:00:00 2001 From: Nick Entin Date: Mon, 22 Apr 2024 23:28:15 -0700 Subject: [PATCH] Support result builder syntax for view spreading --- Paralayout/UIView+Spreading.swift | 59 +++++- Paralayout/ViewArrayBuilder.swift | 90 +++++++++ ParalayoutTests/ViewArrayBuilderTests.swift | 194 ++++++++++++++++++++ 3 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 Paralayout/ViewArrayBuilder.swift create mode 100644 ParalayoutTests/ViewArrayBuilderTests.swift diff --git a/Paralayout/UIView+Spreading.swift b/Paralayout/UIView+Spreading.swift index 4e0f99f..feae4c5 100644 --- a/Paralayout/UIView+Spreading.swift +++ b/Paralayout/UIView+Spreading.swift @@ -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. @@ -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 subviews: The subviews to spread out, ordered from the leading edge to the trailing edge of the + /// receiver. + /// - 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`. + public func horizontallySpreadSubviews( + @ViewArrayBuilder _ subviews: () -> [UIView], + margin: CGFloat, + inRect bounds: CGRect? = nil, + orthogonalBehavior: VerticalSpreadingBehavior = .fill + ) { + 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 @@ -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 subviews: The subviews to spread out, ordered from the top edge to the bottom edge of the receiver. + /// - 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`. + public func verticallySpreadSubviews( + @ViewArrayBuilder _ subviews: () -> [UIView], + margin: CGFloat, + inRect bounds: CGRect? = nil, + orthogonalBehavior: HorizontalSpreadingBehavior = .fill + ) { + 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 diff --git a/Paralayout/ViewArrayBuilder.swift b/Paralayout/ViewArrayBuilder.swift new file mode 100644 index 0000000..3e51dda --- /dev/null +++ b/Paralayout/ViewArrayBuilder.swift @@ -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 diff --git a/ParalayoutTests/ViewArrayBuilderTests.swift b/ParalayoutTests/ViewArrayBuilderTests.swift new file mode 100644 index 0000000..7f1f92d --- /dev/null +++ b/ParalayoutTests/ViewArrayBuilderTests.swift @@ -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