Skip to content

Commit

Permalink
Add DistributionItemsBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed committed Mar 20, 2024
1 parent c8a8bef commit 8ce77f0
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 30 deletions.
8 changes: 6 additions & 2 deletions README.md
Expand Up @@ -72,7 +72,7 @@ For a visual example, check out the [FontMetricsVisualization](RelativityVisuali

Since views need to be laid out flexibly over various iOS device sizes, Relativity has the ability to easily distribute subviews with flexible positioning along an axis.

Subview distribution can be controlled by positioning fixed and flexible spacers in between views. Fixed spaces represent points on screen. Fixed spaces are created by inserting `CGFloatConvertible` (`Int`, `Float`, `CGFloat`, or `Double`) types into the distribution expression, or by initializing a `.fixed(CGFloatConvertible)` enum case directly. Flexible spacers represent proportions of the remaining space in the superview after the subviews and fixed spacers have been accounted for. You can create flexible spacers by surrounding an `Int` with a spring `~` operator, or by initializing the `.flexible(Int)` enum case directly. A `~2~` represents twice the space that `~1~` does. Views, fixed spacers, and flexible spacers are bound together by a bidirectional anchor `<>` operator.
Subview distribution can be controlled by positioning fixed and flexible spacers in between views. Fixed spaces represent points on screen. Fixed spaces are created by inserting `CGFloatConvertible` (`Int`, `Float`, `CGFloat`, or `Double`) types into the distribution expression, or by initializing a `.fixed(CGFloatConvertible)` enum case directly. Flexible spacers represent proportions of the remaining space in the superview after the subviews and fixed spacers have been accounted for. You can create flexible spacers by surrounding an `Int` with a spring `~` operator, or by initializing the `.flexible(Int)` enum case directly. A `~2~` represents twice the space that `~1~` does. Views, fixed spacers, and flexible spacers are bound together by a bidirectional anchor `<>` operator, or via a operatorless result builder.

#### Examples

Expand Down Expand Up @@ -104,7 +104,11 @@ To equally distribute subviews `a`, `b`, and `c` at equal distances along a vert

```swift
superview.distributeSubviewsVertically(within: CGRect(x: 0.0, y: 0.0, width: superview.bounds.midX, height: superview.bounds.height)) {
a <> ~1~ <> b <> ~1~ <> c
a
~1~
b
~1~
c
}
```

Expand Down
4 changes: 4 additions & 0 deletions Relativity.xcodeproj/project.pbxproj
Expand Up @@ -23,6 +23,7 @@
1677F4DC22A98694002393CB /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1677F4D822A98694002393CB /* XCTestManifests.swift */; };
1677F4DD22A98694002393CB /* PixelRounderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1677F4D922A98694002393CB /* PixelRounderTests.swift */; };
3237944A27D7CC87005E89F8 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3237944927D7CC87005E89F8 /* TestHelpers.swift */; };
324FC28A2BAA7CD00058EE30 /* DistributionItemsBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324FC2892BAA7CD00058EE30 /* DistributionItemsBuilder.swift */; };
3713FC091E11BA720075109A /* Relativity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3713FBFF1E11BA720075109A /* Relativity.framework */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -55,6 +56,7 @@
1677F4D822A98694002393CB /* XCTestManifests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = XCTestManifests.swift; path = RelativityTests/XCTestManifests.swift; sourceTree = "<group>"; };
1677F4D922A98694002393CB /* PixelRounderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PixelRounderTests.swift; path = RelativityTests/PixelRounderTests.swift; sourceTree = "<group>"; };
3237944927D7CC87005E89F8 /* TestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TestHelpers.swift; path = RelativityTests/TestHelpers.swift; sourceTree = "<group>"; };
324FC2892BAA7CD00058EE30 /* DistributionItemsBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DistributionItemsBuilder.swift; sourceTree = "<group>"; };
32C0033F27D3DBDC00685A6C /* RelativityVisualization.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = RelativityVisualization.playground; sourceTree = "<group>"; };
3713FBFF1E11BA720075109A /* Relativity.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Relativity.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3713FC081E11BA720075109A /* RelativityTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RelativityTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -115,6 +117,7 @@
1677F4C822A9864D002393CB /* UIFont+Relativity.swift */,
1677F4C922A9864D002393CB /* ViewPosition.swift */,
1677F4CA22A9864D002393CB /* DistributionItem.swift */,
324FC2892BAA7CD00058EE30 /* DistributionItemsBuilder.swift */,
);
path = Relativity;
sourceTree = "<group>";
Expand Down Expand Up @@ -262,6 +265,7 @@
1677F4CD22A9864D002393CB /* UILabel+Relativity.swift in Sources */,
1677F4CF22A9864D002393CB /* ErrorHandler.swift in Sources */,
1677F4CE22A9864D002393CB /* PixelRounder.swift in Sources */,
324FC28A2BAA7CD00058EE30 /* DistributionItemsBuilder.swift in Sources */,
1677F4D522A9864D002393CB /* DistributionItem.swift in Sources */,
1677F4CC22A9864D002393CB /* SubviewDistributor.swift in Sources */,
1677F4D022A9864D002393CB /* Operators.swift in Sources */,
Expand Down
Expand Up @@ -53,17 +53,11 @@ PlaygroundPage.current.liveView = containerView

containerView.distributeSubviewsVertically() {
blueRect
<>
~2~
<>
redRect
<>
20
<>
yellowRect
<>
~1~
<>
~2~
redRect
20
yellowRect
~1~
greenRect
}

Expand All @@ -82,7 +76,11 @@ DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterva
}) { (_) in
UIView.animate(withDuration: 1.0, animations: {
containerView.distributeSubviewsVertically() {
~1~ <> 50 <> blueRect <> ~2~ <> greenRect
~1~
50
blueRect
~2~
greenRect
}
containerView.distributeSubviewsHorizontally() {
~17~ <> redRect <> ~3~ <> yellowRect <> ~17~
Expand Down
92 changes: 92 additions & 0 deletions Sources/Relativity/DistributionItemsBuilder.swift
@@ -0,0 +1,92 @@
//
// DistributionItemsBuilder.swift
// Relativity
//
// Created by Dan Federman on 3/19/24.
// Copyright © 2024 Dan Federman.
//
// 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


@resultBuilder
public struct DistributionItemsBuilder {

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

public static func buildExpression(_ component: DistributionItem) -> [DistributionItem] {
[component]
}
public static func buildExpression(_ component: UIView) -> [DistributionItem] {
[.view(component)]
}
public static func buildExpression(_ component: CGFloatConvertible) -> [DistributionItem] {
[.fixed(component.asCGFloat)]
}
public static func buildExpression(_ component: [DistributionItem?]) -> [DistributionItem] {
component.compactMap { $0 }
}
public static func buildExpression(_ component: [DistributionItem]) -> [DistributionItem] {
component
}
public static func buildExpression(_ component: DistributionItem?) -> [DistributionItem] {
[component].compactMap { $0 }
}

// Build partial results, which accumulate.

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

// Build if statements

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

// Build if-else and switch statements

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

// Build the blocks that turn into results.

public static func buildBlock(_ components: [DistributionItem]...) -> [DistributionItem] {
components.flatMap { $0 }
}
}
4 changes: 2 additions & 2 deletions Sources/Relativity/UIView+Relativity.swift
Expand Up @@ -64,11 +64,11 @@ extension UIView {

// MARK: Public Methods

public func distributeSubviewsVertically(within rect: CGRect = .zero, subviewDistributionCreationBlock: () -> [DistributionItem]) {
public func distributeSubviewsVertically(within rect: CGRect = .zero, @DistributionItemsBuilder subviewDistributionCreationBlock: () -> [DistributionItem]) {
SubviewDistributor.newVerticalSubviewDistributor(with: self).distribute(subviewDistribution: subviewDistributionCreationBlock(), within: rect)
}

public func distributeSubviewsHorizontally(within rect: CGRect = .zero, subviewDistributionCreationBlock: () -> [DistributionItem]) {
public func distributeSubviewsHorizontally(within rect: CGRect = .zero, @DistributionItemsBuilder subviewDistributionCreationBlock: () -> [DistributionItem]) {
SubviewDistributor.newHorizontalSubviewDistributor(with: self).distribute(subviewDistribution: subviewDistributionCreationBlock(), within: rect)
}

Expand Down
59 changes: 45 additions & 14 deletions Tests/RelativityTests/SubviewDistributionTests.swift
Expand Up @@ -38,8 +38,12 @@ class SubviewDistributionTests: XCTestCase {
view.addSubview(b)
view.addSubview(c)

view.distributeSubviewsVertically { () -> [DistributionItem] in
a <> ~1~ <> b <> ~2~ <> c
view.distributeSubviewsVertically {
a
~1~
b
~2~
c
}

// Space before a is implicity ~1~.
Expand Down Expand Up @@ -72,8 +76,12 @@ class SubviewDistributionTests: XCTestCase {
view.addSubview(b)
view.addSubview(c)

view.distributeSubviewsVertically { () -> [DistributionItem] in
a <> ~3~ <> b <> 8 <> c
view.distributeSubviewsVertically {
a
~3~
b
8
c
}

// Space before a is implicity ~1~.
Expand Down Expand Up @@ -107,8 +115,13 @@ class SubviewDistributionTests: XCTestCase {
view.addSubview(b)
view.addSubview(c)

view.distributeSubviewsVertically { () -> [DistributionItem] in
8 <> a <> ~3~ <> b <> 8 <> c
view.distributeSubviewsVertically {
8
a
~3~
b
8
c
}

// Space after c is implicity ~1~.
Expand Down Expand Up @@ -141,8 +154,13 @@ class SubviewDistributionTests: XCTestCase {
view.addSubview(b)
view.addSubview(c)

view.distributeSubviewsVertically { () -> [DistributionItem] in
a <> ~2~ <> b <> 8 <> c <> 16
view.distributeSubviewsVertically {
a
~2~
b
8
c
16
}

// Space before a is implicity ~1~.
Expand Down Expand Up @@ -179,8 +197,12 @@ class SubviewDistributionTests: XCTestCase {
view.addSubview(b)
view.addSubview(c)

view.distributeSubviewsVertically { () -> [DistributionItem] in
a <> 8 <> b <> ~2~ <> c
view.distributeSubviewsVertically {
a
8
b
~2~
c
}

// Space after c is implicity ~1~.
Expand Down Expand Up @@ -215,8 +237,12 @@ class SubviewDistributionTests: XCTestCase {

let subviewDistributionRect = CGRect(x: 75.0, y: 0.0, width: a.bounds.width, height: 200.0)
let verticalDistanceBetweenAToB: CGFloat = 5.0
view.distributeSubviewsVertically(within: subviewDistributionRect) { () -> [DistributionItem] in
a <> verticalDistanceBetweenAToB <> b <> ~2~ <> c
view.distributeSubviewsVertically(within: subviewDistributionRect) {
a
verticalDistanceBetweenAToB
b
~2~
c
}

// Assert that our views were laid out within the right distribution rect.
Expand Down Expand Up @@ -256,8 +282,13 @@ class SubviewDistributionTests: XCTestCase {

let subviewDistributionRect = CGRect(x: 35.0, y: 0.0, width: 220, height: 200.0)
let horizontalDistanceBetweenLeftEdgeAndA: CGFloat = 12.0
view.distributeSubviewsHorizontally(within: subviewDistributionRect) { () -> [DistributionItem] in
horizontalDistanceBetweenLeftEdgeAndA <> a <> ~1~ <> b <> ~4~ <> c
view.distributeSubviewsHorizontally(within: subviewDistributionRect) {
horizontalDistanceBetweenLeftEdgeAndA
a
~1~
b
~4~
c
}

// Assert that our views were laid out within the right distribution rect.
Expand Down

0 comments on commit 8ce77f0

Please sign in to comment.