|
| 1 | +// |
| 2 | +// ViewSpacing.swift |
| 3 | +// OpenSwiftUICore |
| 4 | +// |
| 5 | +// Audited for iOS 18.0 |
| 6 | +// Status: Complete |
| 7 | + |
| 8 | +public import Foundation |
| 9 | + |
| 10 | +/// A collection of the geometric spacing preferences of a view. |
| 11 | +/// |
| 12 | +/// This type represents how much space a view prefers to have between it and |
| 13 | +/// the next view in a layout. The type stores independent values |
| 14 | +/// for each of the top, bottom, leading, and trailing edges, |
| 15 | +/// and can also record different values for different kinds of adjacent |
| 16 | +/// views. For example, it might contain one value for the spacing to the next |
| 17 | +/// text view along the top and bottom edges, other values for the spacing to |
| 18 | +/// text views on other edges, and yet other values for other kinds of views. |
| 19 | +/// Spacing preferences can also vary by platform. |
| 20 | +/// |
| 21 | +/// Your ``Layout`` type doesn't have to take preferred spacing into |
| 22 | +/// account, but if it does, you can use the ``LayoutSubview/spacing`` |
| 23 | +/// preferences of the subviews in your layout container to: |
| 24 | +/// |
| 25 | +/// * Add space between subviews when you implement the |
| 26 | +/// ``Layout/placeSubviews(in:proposal:subviews:cache:)`` method. |
| 27 | +/// * Create a spacing preferences instance for the container view by |
| 28 | +/// implementing the ``Layout/spacing(subviews:cache:)`` method. |
| 29 | +public struct ViewSpacing: Sendable { |
| 30 | + /// The underlying spacing implementation that stores the actual spacing values |
| 31 | + /// for each edge and direction. |
| 32 | + package var spacing: Spacing |
| 33 | + |
| 34 | + /// The layout direction used to resolve relative edges (leading/trailing) |
| 35 | + /// to absolute edges (left/right). |
| 36 | + var layoutDirection: LayoutDirection? |
| 37 | + |
| 38 | + /// Creates a new ViewSpacing instance with the specified spacing values. |
| 39 | + /// |
| 40 | + /// - Parameter spacing: The spacing implementation to use. |
| 41 | + package init(_ spacing: Spacing) { |
| 42 | + self.spacing = spacing |
| 43 | + self.layoutDirection = nil |
| 44 | + } |
| 45 | + |
| 46 | + /// Creates a new ViewSpacing instance with the specified spacing values and layout direction. |
| 47 | + /// |
| 48 | + /// - Parameters: |
| 49 | + /// - spacing: The spacing implementation to use. |
| 50 | + /// - layoutDirection: The layout direction to use when resolving relative edges. |
| 51 | + package init(_ spacing: Spacing, layoutDirection: LayoutDirection) { |
| 52 | + self.spacing = spacing |
| 53 | + self.layoutDirection = layoutDirection |
| 54 | + } |
| 55 | + |
| 56 | + /// A view spacing instance that contains zero on all edges. |
| 57 | + /// |
| 58 | + /// You typically only use this value for an empty view. |
| 59 | + public static let zero: ViewSpacing = ViewSpacing(.zero) |
| 60 | + |
| 61 | + /// Initializes an instance with default spacing values. |
| 62 | + /// |
| 63 | + /// Use this initializer to create a spacing preferences instance with |
| 64 | + /// default values. Then use ``formUnion(_:edges:)`` to combine |
| 65 | + /// preferences from other views with the new instance. You typically |
| 66 | + /// do this in a custom layout's implementation of the |
| 67 | + /// ``Layout/spacing(subviews:cache:)`` method. |
| 68 | + public init() { |
| 69 | + self.spacing = Spacing(minima: [:]) |
| 70 | + self.layoutDirection = nil |
| 71 | + } |
| 72 | + |
| 73 | + /// Merges the spacing preferences of another spacing instance with this |
| 74 | + /// instance for a specified set of edges. |
| 75 | + /// |
| 76 | + /// When you merge another spacing preference instance with this one, |
| 77 | + /// this instance ends up with the greater of its original value or the |
| 78 | + /// other instance's value for each of the specified edges. |
| 79 | + /// You can call the method repeatedly with each value in a collection to |
| 80 | + /// merge a collection of preferences. The result has the smallest |
| 81 | + /// preferences on each edge that meets the largest requirements of all |
| 82 | + /// the inputs for that edge. |
| 83 | + /// |
| 84 | + /// If you want to merge preferences without modifying the original |
| 85 | + /// instance, use ``union(_:edges:)`` instead. |
| 86 | + /// |
| 87 | + /// - Parameters: |
| 88 | + /// - other: Another spacing preferences instances to merge with this one. |
| 89 | + /// - edges: The edges to merge. Edges that you don't specify are |
| 90 | + /// unchanged after the method completes. |
| 91 | + public mutating func formUnion(_ other: ViewSpacing, edges: Edge.Set = .all) { |
| 92 | + let layoutDirection = layoutDirection ?? other.layoutDirection |
| 93 | + self.layoutDirection = layoutDirection |
| 94 | + spacing.incorporate(AbsoluteEdge.Set(edges, layoutDirection: layoutDirection ?? .leftToRight), of: other.spacing) |
| 95 | + } |
| 96 | + |
| 97 | + /// Gets a new value that merges the spacing preferences of another spacing |
| 98 | + /// instance with this instance for a specified set of edges. |
| 99 | + /// |
| 100 | + /// This method behaves like ``formUnion(_:edges:)``, except that it creates |
| 101 | + /// a copy of the original spacing preferences instance before merging, |
| 102 | + /// leaving the original instance unmodified. |
| 103 | + /// |
| 104 | + /// - Parameters: |
| 105 | + /// - other: Another spacing preferences instance to merge with this one. |
| 106 | + /// - edges: The edges to merge. Edges that you don't specify are |
| 107 | + /// unchanged after the method completes. |
| 108 | + /// |
| 109 | + /// - Returns: A new view spacing preferences instance with the merged |
| 110 | + /// values. |
| 111 | + public func union(_ other: ViewSpacing, edges: Edge.Set = .all) -> ViewSpacing { |
| 112 | + var copy = self |
| 113 | + copy.formUnion(other, edges: edges) |
| 114 | + return copy |
| 115 | + } |
| 116 | + |
| 117 | + /// Gets the preferred spacing distance along the specified axis to the view |
| 118 | + /// that returns a specified spacing preference. |
| 119 | + /// |
| 120 | + /// Call this method from your implementation of ``Layout`` protocol |
| 121 | + /// methods if you need to measure the default spacing between two |
| 122 | + /// views in a custom layout. Call the method on the first view's |
| 123 | + /// preferences instance, and provide the second view's preferences |
| 124 | + /// instance as input. |
| 125 | + /// |
| 126 | + /// For example, consider two views that appear in a custom horizontal |
| 127 | + /// stack. The following distance call gets the preferred spacing between |
| 128 | + /// these views, where `spacing1` contains the preferences of a first |
| 129 | + /// view, and `spacing2` contains the preferences of a second view: |
| 130 | + /// |
| 131 | + /// let distance = spacing1.distance(to: spacing2, axis: .horizontal) |
| 132 | + /// |
| 133 | + /// The method first determines, based on the axis and the ordering, that |
| 134 | + /// the views abut on the trailing edge of the first view and the leading |
| 135 | + /// edge of the second. It then gets the spacing preferences for the |
| 136 | + /// corresponding edges of each view, and returns the greater of the two |
| 137 | + /// values. This results in the smallest value that provides enough space |
| 138 | + /// to satisfy the preferences of both views. |
| 139 | + /// |
| 140 | + /// > Note: This method returns the default spacing between views, but a |
| 141 | + /// layout can choose to ignore the value and use custom spacing instead. |
| 142 | + /// |
| 143 | + /// - Parameters: |
| 144 | + /// - next: The spacing preferences instance of the adjacent view. |
| 145 | + /// - axis: The axis that the two views align on. |
| 146 | + /// |
| 147 | + /// - Returns: A floating point value that represents the smallest distance |
| 148 | + /// in points between two views that satisfies the spacing preferences |
| 149 | + /// of both this view and the adjacent views on their shared edge. |
| 150 | + public func distance(to next: ViewSpacing, along axis: Axis) -> CGFloat { |
| 151 | + guard let distance = spacing.distanceToSuccessorView(along: axis, layoutDirection: layoutDirection ?? .leftToRight, preferring: next.spacing) else { |
| 152 | + let defaultSpacingValue = defaultSpacingValue |
| 153 | + return axis == .horizontal ? defaultSpacingValue.width : defaultSpacingValue.height |
| 154 | + } |
| 155 | + return distance |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +@_spi(ForOpenSwiftUIOnly) |
| 160 | +extension ViewSpacing: CustomStringConvertible { |
| 161 | + public var description: String { |
| 162 | + spacing.description |
| 163 | + } |
| 164 | +} |
0 commit comments