1
votes

I'm trying to achieve something that is quite easy in UIKit - one view that is always in in the center (image) and the second view (text) is on top of it with some spacing between two views. I tried many different approaches (mainly using alignmentGuide but nothing worked as I'd like).

code:

ZStack {
    Rectangle()
        .foregroundColor(Color.red)
    
    VStack {
        Text("Test")
            .padding([.bottom], 20) // I want to define spacing between two views
        
        Image(systemName: "circle")
            .resizable()
            .alignmentGuide(VerticalAlignment.center, computeValue: { value in
                value[VerticalAlignment.center] + value.height
            })
            .frame(width: 20, height: 20)
    }
}
.frame(width: 100, height: 100)

result:

enter image description here

As you can see image is not perfectly centered and it actually depends on the padding value of the Text. Is there any way to force vertical and horizontal alignment to be centered in the superview and layout second view without affecting centered view?

3

3 Answers

4
votes

I think the “correct” way to do this is to define a custom alignment:

extension VerticalAlignment {
    static var custom: VerticalAlignment {
        struct CustomAlignment: AlignmentID {
            static func defaultValue(in context: ViewDimensions) -> CGFloat {
                context[VerticalAlignment.center]
            }
        }
        return .init(CustomAlignment.self)
    }
}

Then, tell your ZStack to use the custom alignment, and use alignmentGuide to explicitly set the custom alignment on your circle:

PlaygroundPage.current.setLiveView(
    ZStack(alignment: .init(horizontal: .center, vertical: .custom)) {
        Color.white

        Rectangle()
            .fill(Color.red)
            .frame(width: 100, height: 100)

        VStack {
            Text("Test")
            Circle()
                .stroke(Color.white)
                .frame(width: 20, height: 20)
                .alignmentGuide(.custom, computeValue: { $0.height / 2 })
        }
    }
            .frame(width: 300, height: 300)
)

Result:

enter image description here

2
votes

You can center the Image by moving it to ZStack. Then apply .alignmentGuide to the Text:

struct ContentView: View {
    var body: some View {
        ZStack {
            Rectangle()
                .foregroundColor(Color.red)

            Text("Test")
                .alignmentGuide(VerticalAlignment.center) { $0[.bottom] + $0.height }

            Image(systemName: "circle")
                .resizable()
                .frame(width: 20, height: 20)
        }
        .frame(width: 100, height: 100)
    }
}

Note that as you specify the width/height of the Image explicitly:

Image(systemName: "circle")
    .resizable()
    .frame(width: 20, height: 20)

you can specify the .alignmentGuide explicitly as well:

.alignmentGuide(VerticalAlignment.center) { $0[.bottom] + 50 }
0
votes

Here is possible alternate, using automatic space consuming feature

Tested with Xcode 12 / iOS 14

demo

struct ContentView: View {
    var body: some View {
        ZStack {
            Rectangle()
                .foregroundColor(Color.red)

            VStack(spacing: 0) {
                Color.clear
                    .overlay(
                        Text("Test").padding([.bottom], 10),
                        alignment: .bottom)

                Image(systemName: "circle")
                    .resizable()
                    .frame(width: 20, height: 20)

                Color.clear
            }
        }
        .frame(width: 100, height: 100)
    }
}

Note: before I used Spacer() for such purpose but with Swift 2.0 it appears spacer becomes always just a spacer, ie. nothing can be attached to it - maybe bug.