1

I have 2 HStacks sitting in one VStack as follows:

enter image description here

What I'm trying to achieve is to align the leading of Title 2 and Title 4 with each other. And also Title 1 and Title 3 to each other without using frames. Like the following:

enter image description here

Here is my code using HorizontalAlignment (I have also included the preview for convenience):

import SwiftUI

public struct ContentView: View {
    public var body: some View {
        VStack(alignment: .centerHorizontalAlignment, spacing: 10) {
            HStack(alignment: .top, spacing: 16) {
                VStack(alignment: .leading, spacing: 10) {
                    Text("Title 1")
                        .font(.body)
                        .fontWeight(.medium)

                    Text("123456789")
                        .font(.body)
                        .foregroundColor(.black.opacity(0.6))

                }
                .background(Color.green)

                VStack(alignment: .leading, spacing: 10) {
                    Text("Title 2")
                        .font(.body)
                        .fontWeight(.medium)

                    Text("2 lines\nof text")
                        .font(.body)
                        .foregroundColor(.gray)

                }
                .background(Color.red)
                .alignmentGuide(.centerHorizontalAlignment) { $0[HorizontalAlignment.leading] }

            }
            .padding()
            .background(Color.purple)


            HStack(alignment: .top, spacing: 16) {
                VStack(alignment: .leading, spacing: 10) {
                    Text("Title 3")
                        .font(.body)
                        .fontWeight(.medium)


                    Text("Aaaaaaaaaa")
                        .font(.body)
                        .foregroundColor(.black.opacity(0.6))

                    Text("Bbbbbbbbbbbbbbbbbbbb")
                        .font(.body)
                        .foregroundColor(.black.opacity(0.6))
                }
                .background(Color.yellow)

                VStack(alignment: .leading, spacing: 10) {
                    Text("Title 4")
                        .font(.body)
                        .fontWeight(.medium)


                    Text("Cccccccccccc")
                        .font(.body)
                        .foregroundColor(.black.opacity(0.6))
                }
                .background(Color.blue)
                .alignmentGuide(.centerHorizontalAlignment) { $0[HorizontalAlignment.leading] }

            }
            .font(.body)
            .padding()
            .background(Color.gray)


        }
        .padding()
        .frame(alignment: .leading)
        .background(Color.cyan)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

extension HorizontalAlignment {
    enum CenterHorizontalAlignment: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            d[HorizontalAlignment.center]
        }
    }

    static let centerHorizontalAlignment = HorizontalAlignment(CenterHorizontalAlignment.self)

}

Would be appreciated if someone can give me some hints or solutions on how to achieve this. I'm using Xcode 13.3.1 and Swift 5.

P.S. The text is fully generic and dynamic. So there is no guarantee that there are always 1 or 2 lines of text here. I would like this solution to be generic enough so it works no matter how long the content is.

Mina
  • 2,167
  • 2
  • 25
  • 32

2 Answers2

0

Thanks to Paul B's answer:

public struct ContentView: View {
    @State var title4Width: CGFloat = 0
    @State var title2Width: CGFloat = 0
    public var body: some View {
        VStack( spacing: 10) {
            HStack(alignment: .top, spacing: 16) {
                VStack(alignment: .leading, spacing: 10) {
                    Text("Title 1")
                        .font(.body)
                        .fontWeight(.medium)
                    Text("123456789")
                        .font(.body)
                        .foregroundColor(.black.opacity(0.6))
                }.background(Color.green)
                Spacer()
                VStack(alignment: .leading, spacing: 10) {
                    Text("Title 2")
                        .font(.body)
                        .fontWeight(.medium)
                    Text("2 lines\nof text")
                        .font(.body)
                        .foregroundColor(.gray)
                }.background(Color.red)
                .readSize { size in
                    title2Width = size.width
                }.padding(.trailing, title4Width - title2Width > 0 ? (title4Width - title2Width): 0)
            }.padding()
            .background(Color.purple)
            HStack(alignment: .top, spacing: 16) {
                VStack(alignment: .leading, spacing: 10) {
                    Text("Title 3")
                        .font(.body)
                        .fontWeight(.medium)
                    Text("Aaaaaaaaaa")
                        .font(.body)
                        .foregroundColor(.black.opacity(0.6))
                    Text("Bbbbbbbbbbbbbbbbbbbb")
                        .font(.body)
                        .foregroundColor(.black.opacity(0.6))
                }.background(Color.yellow)
                Spacer()
                VStack(alignment: .leading, spacing: 10) {
                    Text("Title 4")
                        .font(.body)
                        .fontWeight(.medium)
                    Text("Cccccccccccc")
                        .font(.body)
                        .foregroundColor(.black.opacity(0.6))
                }.background(Color.blue)
                .readSize { size in
                    title4Width = size.width
                }.padding(.trailing, title2Width - title4Width > 0 ? (title2Width - title4Width): 0)
            }.font(.body)
            .padding()
            .background(Color.gray)
        }.padding()
        .frame(alignment: .leading)
        .background(Color.blue)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
extension View {
    func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
        background(
            GeometryReader { geometryProxy in
                Color.clear
                    .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
            }
        ).onPreferenceChange(SizePreferenceKey.self, perform: onChange)
    }
}

private struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
Timmy
  • 4,098
  • 2
  • 14
  • 34
  • Thanks, Noe for the answer. I'm not a big fan of using frame and sizing in this kind of situation where the expected text is dynamic. Please replace the ```Text("2 lines\nof text")``` with ```Text("2 lines of text that can even be more")``` to see the result. – Mina Jun 01 '22 at 11:05
-1

I think you want custom alignment guides: https://www.answertopia.com/swiftui/swiftui-stack-alignment-and-alignment-guides/

  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jun 02 '22 at 22:08