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

Create ViewDistributionBuilder #117

Merged
merged 7 commits into from
Apr 23, 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
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