1

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?

mikro098
  • 2,173
  • 2
  • 32
  • 48

3 Answers3

5

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

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
2

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 }
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • making image frame bigger gives overlap with text, because they actually do not align to each other. – Asperi Aug 07 '20 at 15:02
  • @Asperi True, I assumed that if OP decided to specify the frame width/height explicitly they can specify the alignmentGuide as well. – pawello2222 Aug 07 '20 at 15:07
  • @pawello2222 it's a nice solution and I will probably stick to it for now but as Asperi noticed - it's not without a flaw. As I said in the question - I would like to have image centered in its superview and be able to align other view to it (padding or something else) – mikro098 Aug 14 '20 at 07:11
  • 1
    @mikro098 And this is exactly what I proposed in my answer. You *can* align everything as you wish. But in the long run I'd recommend you use [rob mayoff's answer](https://stackoverflow.com/a/63304896/8697793) - it's more complicated but once you understand it, it will allow you to align views using their *relative* position. As always I suggest you read this excellent tutorial: [Alignment Guides in SwiftUI](https://swiftui-lab.com/alignment-guides/) – pawello2222 Aug 14 '20 at 10:11
0

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.

Asperi
  • 228,894
  • 20
  • 464
  • 690