SwiftUI 如何动态地使视图的领先等于其到超级视图的顶部距离

SwiftUI How to dynamically make a view's leading equal to its top distance to superview

下图显示了包含三行文本的 VStack

VStack 位于 ZStack 内,该 ZStack 还包含一个蓝色方块。

我已经能够定位蓝色方块,使其垂直中心等于显示“第 1 行”的 Text 的垂直中心。无论文本大小如何,都必须保持这种关系。换句话说,我无法为蓝色方块的垂直偏移量硬编码一个特定值,因为它取决于其右侧 Text 的大小。

注意文本上方和下方的绿线。这表示容器视图的顶部和底部边缘。

我想做的(但不知道怎么做)是将蓝色方块的前缘定位到与蓝色方块的上边缘到顶部绿线的距离相同的右侧。

例如,假设将蓝色框垂直放置后,其顶部恰好比顶部绿线低 12 磅。在这种情况下,盒子应该向容器左边缘的右侧移动 12 磅。

这是我正在处理的代码:

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)
    }
}

可能的方法是使用锚首选项,因为它们允许读取目标视图的不同位置属性并在视图布局周期内使用。

这是一个演示。使用 Xcode 13.2 / iOS 15.2

测试

注意:使用padding而不是offset,因为偏移不影响布局,以防万一。

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 !!
                ) { [[=10=]] }
                .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 !!
                }
            }
        }
    }
}