Skip to content

Commit 9205c20

Browse files
committed
Add View.alignmentGuide
1 parent a07f255 commit 9205c20

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// AlignmentGuideUITests.swift
3+
// OpenSwiftUIUITests
4+
5+
import SnapshotTesting
6+
import Testing
7+
8+
@MainActor
9+
@Suite(.snapshots(record: .never, diffTool: diffTool))
10+
struct AlignmentGuideUITests {
11+
@Test
12+
func alignmentGuideOnXY() {
13+
struct ContentView: View {
14+
var body: some View {
15+
VStack(alignment: .leading) {
16+
ForEach(0 ..< 4) { index in
17+
Color.red.opacity(Double(index) / 6.0 )
18+
.alignmentGuide(.leading) { _ in CGFloat(index) * -10 }
19+
}
20+
}
21+
22+
VStack(alignment: .leading) {
23+
ForEach(0 ..< 4) { index in
24+
Color.red.opacity(Double(index) / 6.0 )
25+
.alignmentGuide(.trailing) { _ in CGFloat(index) * 10 }
26+
}
27+
}
28+
29+
HStack(alignment: .top) {
30+
ForEach(0 ..< 4) { index in
31+
Color.red.opacity(Double(index) / 6.0 )
32+
.alignmentGuide(.top) { _ in CGFloat(index) * -10 }
33+
}
34+
}
35+
36+
HStack(alignment: .top) {
37+
ForEach(0 ..< 4) { index in
38+
Color.red.opacity(Double(index) / 6.0 )
39+
.alignmentGuide(.bottom) { _ in CGFloat(index) * 10 }
40+
}
41+
}
42+
}
43+
}
44+
openSwiftUIAssertSnapshot(of: ContentView())
45+
}
46+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//
2+
// AlignmentWritingModifier.swift
3+
// OpenSwiftUI
4+
//
5+
// Audited for 6.5.4
6+
// Status: Complete
7+
// ID: 3A1D0350CBB400C95A809DBE8B845F0C (SwiftUI)
8+
9+
import OpenAttributeGraphShims
10+
public import OpenCoreGraphicsShims
11+
public import OpenSwiftUICore
12+
13+
// MARK: - _AlignmentWritingModifier
14+
15+
struct _AlignmentWritingModifier: MultiViewModifier, PrimitiveViewModifier {
16+
let key: AlignmentKey
17+
let computeValue: @Sendable (ViewDimensions) -> CGFloat
18+
19+
init(key: AlignmentKey, computeValue: @Sendable @escaping (ViewDimensions) -> CGFloat) {
20+
self.key = key
21+
self.computeValue = computeValue
22+
}
23+
24+
static func _makeView(
25+
modifier: _GraphValue<Self>,
26+
inputs: _ViewInputs,
27+
body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs
28+
) -> _ViewOutputs {
29+
var outputs = body(_Graph(), inputs)
30+
if inputs.requestsLayoutComputer {
31+
outputs.layoutComputer = Attribute(AlignmentModifiedLayoutComputer(
32+
modifier: modifier.value,
33+
childLayoutComputer: OptionalAttribute(outputs.layoutComputer)
34+
))
35+
}
36+
return outputs
37+
}
38+
}
39+
40+
// MARK: - AlignmentModifiedLayoutComputer
41+
42+
private struct AlignmentModifiedLayoutComputer: StatefulRule, AsyncAttribute {
43+
@Attribute var modifier: _AlignmentWritingModifier
44+
@OptionalAttribute var childLayoutComputer: LayoutComputer?
45+
46+
typealias Value = LayoutComputer
47+
48+
mutating func updateValue() {
49+
update(to: Engine(
50+
modifier: modifier,
51+
childLayoutComputer: childLayoutComputer ?? .defaultValue
52+
))
53+
}
54+
55+
struct Engine: LayoutEngine {
56+
var modifier: _AlignmentWritingModifier
57+
var childLayoutComputer: LayoutComputer
58+
59+
func sizeThatFits(_ proposedSize: _ProposedSize) -> CGSize {
60+
childLayoutComputer.sizeThatFits(proposedSize)
61+
}
62+
63+
func explicitAlignment(_ key: AlignmentKey, at size: ViewSize) -> CGFloat? {
64+
guard key == modifier.key else {
65+
return childLayoutComputer.explicitAlignment(key, at: size)
66+
}
67+
let dimensions = ViewDimensions(guideComputer: childLayoutComputer, size: size)
68+
return modifier.computeValue(dimensions)
69+
}
70+
}
71+
}
72+
73+
extension View {
74+
/// Sets the view's horizontal alignment.
75+
///
76+
/// Use `alignmentGuide(_:computeValue:)` to calculate specific offsets
77+
/// to reposition views in relationship to one another. You can return a
78+
/// constant or can use the ``ViewDimensions`` argument to the closure to
79+
/// calculate a return value.
80+
///
81+
/// In the example below, the ``HStack`` is offset by a constant of 50
82+
/// points to the right of center:
83+
///
84+
/// VStack {
85+
/// Text("Today's Weather")
86+
/// .font(.title)
87+
/// .border(.gray)
88+
/// HStack {
89+
/// Text("🌧")
90+
/// Text("Rain & Thunderstorms")
91+
/// Text("⛈")
92+
/// }
93+
/// .alignmentGuide(HorizontalAlignment.center) { _ in 50 }
94+
/// .border(.gray)
95+
/// }
96+
/// .border(.gray)
97+
///
98+
/// Changing the alignment of one view may have effects on surrounding
99+
/// views. Here the offset values inside a stack and its contained views is
100+
/// the difference of their absolute offsets.
101+
///
102+
/// ![A view showing the two emoji offset from a text element using a
103+
/// horizontal alignment guide.](SwiftUI-View-HAlignmentGuide.png)
104+
///
105+
/// - Parameters:
106+
/// - g: A ``HorizontalAlignment`` value at which to base the offset.
107+
/// - computeValue: A closure that returns the offset value to apply to
108+
/// this view.
109+
///
110+
/// - Returns: A view modified with respect to its horizontal alignment
111+
/// according to the computation performed in the method's closure.
112+
@preconcurrency
113+
nonisolated public func alignmentGuide(
114+
_ g: HorizontalAlignment,
115+
computeValue: @escaping (ViewDimensions) -> CGFloat
116+
) -> some View {
117+
modifier(_AlignmentWritingModifier(key: g.key, computeValue: computeValue))
118+
}
119+
120+
/// Sets the view's vertical alignment.
121+
///
122+
/// Use `alignmentGuide(_:computeValue:)` to calculate specific offsets
123+
/// to reposition views in relationship to one another. You can return a
124+
/// constant or can use the ``ViewDimensions`` argument to the closure to
125+
/// calculate a return value.
126+
///
127+
/// In the example below, the weather emoji are offset 20 points from the
128+
/// vertical center of the ``HStack``.
129+
///
130+
/// VStack {
131+
/// Text("Today's Weather")
132+
/// .font(.title)
133+
/// .border(.gray)
134+
///
135+
/// HStack {
136+
/// Text("🌧")
137+
/// .alignmentGuide(VerticalAlignment.center) { _ in -20 }
138+
/// .border(.gray)
139+
/// Text("Rain & Thunderstorms")
140+
/// .border(.gray)
141+
/// Text("⛈")
142+
/// .alignmentGuide(VerticalAlignment.center) { _ in 20 }
143+
/// .border(.gray)
144+
/// }
145+
/// }
146+
///
147+
/// Changing the alignment of one view may have effects on surrounding
148+
/// views. Here the offset values inside a stack and its contained views is
149+
/// the difference of their absolute offsets.
150+
///
151+
/// ![A view showing the two emoji offset from a text element using a
152+
/// vertical alignment guide.](SwiftUI-View-VAlignmentGuide.png)
153+
///
154+
/// - Parameters:
155+
/// - g: A ``VerticalAlignment`` value at which to base the offset.
156+
/// - computeValue: A closure that returns the offset value to apply to
157+
/// this view.
158+
///
159+
/// - Returns: A view modified with respect to its vertical alignment
160+
/// according to the computation performed in the method's closure.
161+
@preconcurrency
162+
nonisolated public func alignmentGuide(
163+
_ g: VerticalAlignment,
164+
computeValue: @escaping (ViewDimensions) -> CGFloat
165+
) -> some View {
166+
modifier(_AlignmentWritingModifier(key: g.key, computeValue: computeValue))
167+
}
168+
}

0 commit comments

Comments
 (0)