The image below shows a VStack
with three lines of text.
The VStack
is inside a ZStack
that also contains a blue square.
I have been able to position the blue square such that its vertical center is equal to the vertical center of the Text
that reads "Line 1". This relationship has to be maintained regardless of the text size. In other words, I can't hardcode a specific value for the blue square's vertical offset because it depends on the size of the Text
to its right.
Notice the green lines above and below the text. This represents the top and bottom edges of the container view.
What I'd like to be able to do (but can't figure out how) is position the blue square's leading edge the same distance to the right as the blue square's top edge is to the top green line.
For example, say after the blue box is positioned vertically its top happens to be 12 pts down from the top green line. In that case the box should move 12 pts to the right of the container's left edge.
Here is the code I've working on:
import SwiftUI
extension Alignment {
static let blueBoxAlignment = Alignment(horizontal: .blueBoxLeadingAlignment, vertical: .blueBoxCenterAlignment)
}
extension HorizontalAlignment {
private enum BlueBoxHorizontalAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[HorizontalAlignment.leading]
}
}
static let blueBoxLeadingAlignment = HorizontalAlignment(BlueBoxHorizontalAlignment.self)
}
extension VerticalAlignment {
private enum BlueBoxCenterAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[VerticalAlignment.center]
}
}
static let blueBoxCenterAlignment = VerticalAlignment(BlueBoxCenterAlignment.self)
}
struct TestView: View {
var body: some View {
ZStack(alignment: .blueBoxAlignment) {
VStack(spacing: 50) {
Text("Line 1")
.alignmentGuide(.blueBoxCenterAlignment) { d in d[VerticalAlignment.center] }
Text("Line 2")
Text("Line 3")
}
.padding([.top, .bottom], 50)
.frame(maxWidth: .infinity)
.border(.green)
Rectangle()
.fill(.blue)
.opacity(0.5)
.frame(width: 50, height: 50)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
.edgesIgnoringSafeArea(.bottom)
}
}
Possible approach is to use anchor preferences, because they allows to read different position properties of targeted view and use within views layout cycle.
Here is a demo. Tested with Xcode 13.2 / iOS 15.2
Note: used padding
instead of offset
, because offset does not affect layout, just in case.
struct PositionPreferenceKey: PreferenceKey { // << helper key !!
static var defaultValue: [Anchor<CGPoint>] = [] // << use something persistent
static func reduce(value: inout [Anchor<CGPoint>], nextValue: () -> [Anchor<CGPoint>]) {
value.append(contentsOf:nextValue())
}
}
struct TestView: View {
@State private var offset = CGFloat.zero
var body: some View {
ZStack(alignment: .blueBoxAlignment) {
VStack(spacing: 50) {
Text("Line 1")
.alignmentGuide(.blueBoxCenterAlignment) { d in d[VerticalAlignment.center] }
Text("Line 2")
Text("Line 3")
}
.padding([.top, .bottom], 50)
.frame(maxWidth: .infinity)
.border(.green)
Rectangle()
.fill(.blue)
.opacity(0.5)
.frame(width: 50, height: 50)
.anchorPreference(
key: PositionPreferenceKey.self,
value: .top // read position from top !!
) { [$0] }
.padding(.leading, offset) // << apply as X !!
}
.backgroundPreferenceValue(PositionPreferenceKey.self) { prefs in
GeometryReader { gr in
Color.clear.onAppear {
self.offset = gr[prefs[0]].y // << store Y !!
}
}
}
}
}