Skip to content

Commit

Permalink
Create ViewDistributionBuilder (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed authored Apr 23, 2024
1 parent a1f0be7 commit e5e3185
Show file tree
Hide file tree
Showing 3 changed files with 506 additions and 0 deletions.
207 changes: 207 additions & 0 deletions Paralayout/UIView+Distribution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,109 @@ extension UIView {
}
}

#if swift(>=5.4)
/// Arranges subviews along the vertical axis according to a distribution with fixed and/or flexible spacers.
///
/// * If there are no flexible elements, this will treat the distribution as vertically centered (i.e. with two
/// flexible elements of equal weight at the top and bottom, respectively).
/// * If there are no spacers (fixed or flexible), this will treat the distribution as equal flexible spacing
/// at the top, bottom, and between each view.
///
/// **Examples:**
///
/// To stack two elements with a 10 pt margin between them:
/// ```
/// // This is effectively the same as [ 1.flexible, icon, 10.fixed, label, 1.flexible ].
/// applyVerticalSubviewDistribution {
/// icon
/// 10.fixed
/// label
/// }
/// ```
///
/// To evenly spread out items:
/// ```
/// // This is effectively the same as [ 1.flexible, button1, 1.flexible, button2, 1.flexible, button3 ].
/// applyVerticalSubviewDistribution {
/// button1
/// button2
/// button3
/// }
/// ```
///
/// To stack two elements with 50% more space below than above:
/// ```
/// applyVerticalSubviewDistribution {
/// 2.flexible
/// label
/// 12.fixed
/// textField
/// 3.flexible
/// }
/// ```
///
/// To arrange a pair of label on the top and bottom edges of a view, with another label centered between them:
/// ```
/// applyVerticalSubviewDistribution {
/// 8.fixed
/// headerLabel
/// 1.flexible
/// bodyLabel
/// 1.flexible
/// footerLabel
/// 8.fixed
/// }
/// ```
///
/// To arrange UI in a view with an interior margin:
/// ```
/// applyVerticalSubviewDistribution {
/// icon
/// 10.fixed
/// label
/// }, inRect: bounds.insetBy(dx: 20, dy: 40))
/// ```
///
/// To arrange UI vertically aligned by their leading edge 10 pt in from the leading edge of their superview:
/// ```
/// applyVerticalSubviewDistribution {
/// icon
/// 1.flexible
/// button
/// }, orthogonalOffset: .leading(inset: 10))
/// ```
///
/// To arrange UI vertically without simultaneously centering it horizontally (the `icon` would need independent
/// horizontal positioning):
/// ```
/// applyVerticalSubviewDistribution {
/// 1.flexible
/// icon
/// 2.flexible
/// }, orthogonalOffset: nil)
/// ```
///
/// - precondition: All views in the `distribution` must be subviews of the receiver.
/// - precondition: The `distribution` must not include any given view more than once.
///
/// - parameter distribution: An array of distribution specifiers, ordered from the top edge to the bottom edge.
/// - parameter layoutBounds: The region in the receiver in which to distribute the view in the receiver's
/// coordinate space. Specify `nil` to use the receiver's bounds. Defaults to `nil`.
/// - parameter orthogonalAlignment: The horizontal alignment to apply to the views. If `nil`, views are left in
/// their horizontal position prior to the distribution. Defaults to centered with no offset.
public func applyVerticalSubviewDistribution(
@ViewDistributionBuilder _ distribution: () -> [ViewDistributionSpecifying],
inRect layoutBounds: CGRect? = nil,
orthogonalAlignment: HorizontalDistributionAlignment? = .centered(offset: 0)
) {
applyVerticalSubviewDistribution(
distribution(),
inRect: layoutBounds,
orthogonalAlignment: orthogonalAlignment
)
}
#endif

/// Arranges subviews along the horizontal axis according to a distribution with fixed and/or flexible spacers.
///
/// * If there are no flexible elements, this will treat the distribution as horizontally centered (i.e. with two
Expand Down Expand Up @@ -229,6 +332,110 @@ extension UIView {
}
}

#if swift(>=5.4)
/// Arranges subviews along the horizontal axis according to a distribution with fixed and/or flexible spacers.
///
/// * If there are no flexible elements, this will treat the distribution as horizontally centered (i.e. with two
/// flexible elements of equal weight at the leading and trailing edges, respectively).
/// * If there are no spacers (fixed or flexible), this will treat the distribution as equal flexible spacing
/// at the leading edge, trailing edge, and between each view.
///
/// **Examples:**
///
/// To stack two elements with a 10 pt margin between them:
/// ```
/// // This is effectively the same as [ 1.flexible, icon, 10.fixed, label, 1.flexible ].
/// applyHorizontalSubviewDistribution {
/// icon
/// 10.fixed
/// label
/// }
/// ```
///
/// To evenly spread out items:
/// ```
/// // This is effectively the same as [ 1.flexible, button1, 1.flexible, button2, 1.flexible, button3 ].
/// applyHorizontalSubviewDistribution {
/// button1
/// button2
/// button3
/// }
/// ```
///
/// To stack two elements with 50% more space after than before:
/// ```
/// applyHorizontalSubviewDistribution {
/// 2.flexible
/// label
/// 12.fixed
/// textField
/// 3.flexible
/// }
/// ```
///
/// To arrange a pair of buttons on the left and right edges of a view, with a label centered between them:
/// ```
/// applyHorizontalSubviewDistribution {
/// 8.fixed
/// backButton
/// 1.flexible
/// titleLabel
/// 1.flexible
/// nextButton
/// 8.fixed
/// }
/// ```
///
/// To arrange UI in a view with an interior margin:
/// ```
/// applyHorizontalSubviewDistribution {
/// icon
/// 10.fixed
/// label
/// }, inRect: bounds.insetBy(dx: 20, dy: 40))
/// ```
///
/// To arrange UI horizontally aligned by their top edge 10 pt in from the top edge of their superview:
/// ```
/// applyHorizontalSubviewDistribution {
/// icon
/// 1.flexible
/// button
/// }, orthogonalOffset: .top(inset: 10))
/// ```
///
/// To arrange UI horizontally without simultaneously centering it vertically (the `icon` would need independent
/// vertical positioning):
/// ```
/// applyHorizontalSubviewDistribution {
/// 1.flexible
/// icon
/// 2.flexible
/// }, orthogonalOffset: nil)
/// ```
///
/// - precondition: All views in the `distribution` must be subviews of the receiver.
/// - precondition: The `distribution` must not include any given view more than once.
///
/// - parameter distribution: An array of distribution specifiers, ordered from the leading edge to the trailing
/// edge.
/// - parameter layoutBounds: The region in the receiver in which to distribute the view in the receiver's
/// coordinate space. Specify `nil` to use the receiver's bounds. Defaults to `nil`.
/// - parameter orthogonalAlignment: The vertical alignment to apply to the views. If `nil`, views are left in
/// their vertical position prior to the distribution. Defaults to centered with no offset.
public func applyHorizontalSubviewDistribution(
@ViewDistributionBuilder _ distribution: () -> [ViewDistributionSpecifying],
inRect layoutBounds: CGRect? = nil,
orthogonalAlignment: VerticalDistributionAlignment? = .centered(offset: 0)
) {
applyHorizontalSubviewDistribution(
distribution(),
inRect: layoutBounds,
orthogonalAlignment: orthogonalAlignment
)
}
#endif

// MARK: - Private Methods

private func applySubviewDistribution(
Expand Down
89 changes: 89 additions & 0 deletions Paralayout/ViewDistributionBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// Copyright © 2024 Square, 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 ViewDistributionBuilder {

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

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

// Build partial results, which accumulate.

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

// Build if statements

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

// Build if-else and switch statements

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

// Build for-loop statements

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

// Build the blocks that turn into results.

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

0 comments on commit e5e3185

Please sign in to comment.