Skip to content

Commit 2771461

Browse files
authored
Add ProposedViewSize and LayoutTraits API (#195)
* Add ProposedViewSize * Add LayoutTraits
1 parent c1be12f commit 2771461

File tree

3 files changed

+411
-0
lines changed

3 files changed

+411
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
//
2+
// LayoutTraits.swift
3+
// OpenSwiftUICore
4+
//
5+
// Audited for iOS 18.0
6+
// Status: Complete
7+
// ID: 950FC9541E969A331FB3CF1283EA4AEC (SwiftUICore)
8+
9+
package import Foundation
10+
11+
/// A description of the sizing behavior of a view.
12+
public struct _LayoutTraits: Equatable {
13+
package struct FlexibilityEstimate: Comparable {
14+
let minLength: CGFloat
15+
let maxLength: CGFloat
16+
17+
package init(minLength: CGFloat, maxLength: CGFloat) {
18+
self.minLength = minLength
19+
self.maxLength = maxLength
20+
}
21+
22+
package static func < (l: _LayoutTraits.FlexibilityEstimate, r: _LayoutTraits.FlexibilityEstimate) -> Bool {
23+
let lDiff = l.maxLength - l.minLength
24+
let rDiff = r.maxLength - r.minLength
25+
26+
let lEffectiveMin = lDiff == .infinity ? -l.minLength : 0
27+
let rEffectiveMin = rDiff == .infinity ? -r.minLength : 0
28+
29+
if lDiff == rDiff {
30+
return lEffectiveMin < rEffectiveMin
31+
} else {
32+
return lDiff < rDiff
33+
}
34+
}
35+
}
36+
37+
package struct Dimension : Equatable {
38+
package var min: CGFloat {
39+
didSet { _checkInvariant() }
40+
}
41+
42+
package var ideal: CGFloat {
43+
didSet { _checkInvariant() }
44+
}
45+
46+
package var max: CGFloat {
47+
didSet { _checkInvariant() }
48+
}
49+
50+
package init(min: CGFloat, ideal: CGFloat, max: CGFloat) {
51+
self.min = min
52+
self.ideal = ideal
53+
self.max = max
54+
_checkInvariant()
55+
}
56+
57+
package static func fixed(_ d: CGFloat) -> Dimension {
58+
Dimension(min: d, ideal: d, max: d)
59+
}
60+
61+
private func _checkInvariant() {
62+
guard min >= 0,
63+
min.isFinite,
64+
ideal < .infinity,
65+
min <= ideal,
66+
ideal <= max
67+
else {
68+
preconditionFailure("malformed dimension \(self)")
69+
}
70+
}
71+
}
72+
73+
package var width = Dimension(min: .zero, ideal: .zero, max: .infinity)
74+
75+
package var height = Dimension(min: .zero, ideal: .zero, max: .infinity)
76+
77+
package init() {}
78+
79+
package init(width: Dimension, height: Dimension) {
80+
self.width = width
81+
self.height = height
82+
}
83+
84+
package subscript(axis: Axis) -> Dimension {
85+
get { axis == .horizontal ? width : height }
86+
set { if axis == .horizontal { width = newValue } else { height = newValue } }
87+
}
88+
}
89+
90+
@available(*, unavailable)
91+
extension _LayoutTraits: Sendable {}
92+
93+
extension _LayoutTraits {
94+
package init(width: CGFloat, height: CGFloat) {
95+
self.width = .fixed(width)
96+
self.height = .fixed(height)
97+
}
98+
99+
package init(_ size: CGSize) {
100+
self.init(width: size.width, height: size.height)
101+
}
102+
}
103+
104+
extension _LayoutTraits: CustomStringConvertible {
105+
public var description: String {
106+
"(\(width), \(height)"
107+
}
108+
}
109+
110+
extension _LayoutTraits.Dimension: CustomStringConvertible {
111+
package var description: String {
112+
if min == max {
113+
"\(min)"
114+
} else {
115+
"\(min)...\(ideal)...\(max)"
116+
}
117+
}
118+
}
119+
120+
extension _LayoutTraits {
121+
package var idealSize: CGSize {
122+
get {
123+
CGSize(width: width.ideal, height: height.ideal)
124+
}
125+
set {
126+
width.ideal = newValue.width
127+
height.ideal = newValue.height
128+
}
129+
}
130+
131+
package var minSize: CGSize {
132+
get {
133+
CGSize(width: width.min, height: height.min)
134+
}
135+
set {
136+
width.min = newValue.width
137+
height.min = newValue.height
138+
}
139+
}
140+
141+
package var maxSize: CGSize {
142+
get {
143+
CGSize(width: width.max, height: height.max)
144+
}
145+
set {
146+
width.max = newValue.width
147+
height.max = newValue.height
148+
}
149+
}
150+
}
151+
152+
extension CGSize {
153+
package func clamped(to constraints: _LayoutTraits) -> CGSize {
154+
CGSize(
155+
width: width.clamp(min: constraints.width.min, max: constraints.width.max),
156+
height: height.clamp(min: constraints.height.min, max: constraints.height.max)
157+
)
158+
}
159+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//
2+
// ProposedViewSize.swift
3+
// OpenSwiftUICore
4+
//
5+
// Audited for iOS 18.0
6+
// Status: Complete
7+
8+
public import Foundation
9+
#if canImport(Darwin)
10+
public import CoreGraphics
11+
#endif
12+
13+
/// A proposal for the size of a view.
14+
///
15+
/// During layout in OpenSwiftUI, views choose their own size, but they do that
16+
/// in response to a size proposal from their parent view. When you create
17+
/// a custom layout using the ``Layout`` protocol, your layout container
18+
/// participates in this process using `ProposedViewSize` instances.
19+
/// The layout protocol's methods take a proposed size input that you
20+
/// can take into account when arranging views and calculating the size of
21+
/// the composite container. Similarly, your layout proposes a size to each
22+
/// of its own subviews when it measures and places them.
23+
///
24+
/// Layout containers typically measure their subviews by proposing several
25+
/// sizes and looking at the responses. The container can use this information
26+
/// to decide how to allocate space among its subviews. A
27+
/// layout might try the following special proposals:
28+
///
29+
/// * The ``zero`` proposal; the view responds with its minimum size.
30+
/// * The ``infinity`` proposal; the view responds with its maximum size.
31+
/// * The ``unspecified`` proposal; the view responds with its ideal size.
32+
///
33+
/// A layout might also try special cases for one dimension at a time. For
34+
/// example, an ``HStack`` might measure the flexibility of its subviews'
35+
/// widths, while using a fixed value for the height.
36+
@frozen
37+
public struct ProposedViewSize: Equatable {
38+
/// The proposed horizontal size measured in points.
39+
///
40+
/// A value of `nil` represents an unspecified width proposal, which a view
41+
/// interprets to mean that it should use its ideal width.
42+
public var width: CGFloat?
43+
44+
/// The proposed vertical size measured in points.
45+
///
46+
/// A value of `nil` represents an unspecified height proposal, which a view
47+
/// interprets to mean that it should use its ideal height.
48+
public var height: CGFloat?
49+
50+
/// A size proposal that contains zero in both dimensions.
51+
///
52+
/// Subviews of a custom layout return their minimum size when you propose
53+
/// this value using the ``LayoutSubview/dimensions(in:)`` method.
54+
/// A custom layout should also return its minimum size from the
55+
/// ``Layout/sizeThatFits(proposal:subviews:cache:)`` method for this
56+
/// value.
57+
public static let zero: ProposedViewSize = .init(width: .zero, height: .zero)
58+
59+
/// The proposed size with both dimensions left unspecified.
60+
///
61+
/// Both dimensions contain `nil` in this size proposal.
62+
/// Subviews of a custom layout return their ideal size when you propose
63+
/// this value using the ``LayoutSubview/dimensions(in:)`` method.
64+
/// A custom layout should also return its ideal size from the
65+
/// ``Layout/sizeThatFits(proposal:subviews:cache:)`` method for this
66+
/// value.
67+
public static let unspecified: ProposedViewSize = .init(width: nil, height: nil)
68+
69+
/// A size proposal that contains infinity in both dimensions.
70+
///
71+
/// Both dimensions contain
72+
/// [infinity](https://developer.apple.com/documentation/CoreFoundation/CGFloat/1454161-infinity)
73+
/// in this size proposal.
74+
/// Subviews of a custom layout return their maximum size when you propose
75+
/// this value using the ``LayoutSubview/dimensions(in:)`` method.
76+
/// A custom layout should also return its maximum size from the
77+
/// ``Layout/sizeThatFits(proposal:subviews:cache:)`` method for this
78+
/// value.
79+
public static let infinity: ProposedViewSize = .init(width: .infinity, height: .infinity)
80+
81+
/// Creates a new proposed size using the specified width and height.
82+
///
83+
/// - Parameters:
84+
/// - width: A proposed width in points. Use a value of `nil` to indicate
85+
/// that the width is unspecified for this proposal.
86+
/// - height: A proposed height in points. Use a value of `nil` to
87+
/// indicate that the height is unspecified for this proposal.
88+
@inlinable
89+
public init(width: CGFloat?, height: CGFloat?) {
90+
(self.width, self.height) = (width, height)
91+
}
92+
93+
package init(_ proposal: _ProposedSize) {
94+
width = proposal.width
95+
height = proposal.height
96+
}
97+
98+
/// Creates a new proposed size from a specified size.
99+
///
100+
/// - Parameter size: A proposed size with dimensions measured in points.
101+
@inlinable
102+
public init(_ size: CGSize) {
103+
self.init(width: size.width, height: size.height)
104+
}
105+
106+
/// Creates a new proposal that replaces unspecified dimensions in this
107+
/// proposal with the corresponding dimension of the specified size.
108+
///
109+
/// Use the default value to prevent a flexible view from disappearing
110+
/// into a zero-sized frame, and ensure the unspecified value remains
111+
/// visible during debugging.
112+
///
113+
/// - Parameter size: A set of concrete values to use for the size proposal
114+
/// in place of any unspecified dimensions. The default value is `10`
115+
/// for both dimensions.
116+
///
117+
/// - Returns: A new, fully specified size proposal.
118+
@inlinable
119+
public func replacingUnspecifiedDimensions(by size: CGSize = CGSize(width: 10, height: 10)) -> CGSize {
120+
CGSize(width: width ?? size.width, height: height ?? size.height)
121+
}
122+
123+
package init(_ major: CGFloat?, in axis: Axis, by minor: CGFloat?) {
124+
self = axis == .horizontal ? ProposedViewSize(width: major, height: minor) : ProposedViewSize(width: minor, height: major)
125+
}
126+
127+
package subscript(axis: Axis) -> CGFloat? {
128+
get { axis == .horizontal ? width : height }
129+
set { if axis == .horizontal { width = newValue } else { height = newValue } }
130+
}
131+
}
132+
133+
extension _ProposedSize {
134+
package init(_ p: ProposedViewSize) {
135+
self.init(width: p.width, height: p.height)
136+
}
137+
}

0 commit comments

Comments
 (0)