简体   繁体   中英

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

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

backup

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM