Layout in SwiftUI with horizontal and vertical alignment

后端 未结 6 2071
别跟我提以往
别跟我提以往 2021-02-04 09:11

I\'m trying to accomplish this layout

If I try HStack wrapped in VStack, I get this:

If I try VStack wrapped in HStack, I get this:

相关标签:
6条回答
  • 2021-02-04 09:27
        var body: some View {
    
        VStack {
            HStack {
                Text("Username")
                Spacer()
                TextField($username)
                    .textFieldStyle(.roundedBorder)
                    .frame(maxWidth: 200)
                    .foregroundColor(.gray)
                    .accentColor(.red)
            }
            .padding(.horizontal, 20)
            HStack {
                Text("Email")
                Spacer()
                TextField($email)
                    .textFieldStyle(.roundedBorder)
                    .frame(maxWidth: 200)
                    .foregroundColor(.gray)
                }
                .padding(.horizontal, 20)
            HStack {
                Text("Password")
                Spacer()
                TextField($password)
                    .textFieldStyle(.roundedBorder)
                    .frame(maxWidth: 200)
                    .foregroundColor(.gray)
                }
                .padding(.horizontal, 20)
        }
    }
    

    0 讨论(0)
  • 2021-02-04 09:36

    not an expert here, but I managed to achieve the desired layout by (1) opting for the 2-VStacks-in-a-HStack alternative, (2) framing the external labels, (3) freeing them from their default vertical expansion constraint by assigning their maxHeight = .infinity and (4) fixing the height of the HStack

    struct ContentView: View {
        @State var text = ""
        let labels = ["Username", "Email", "Password"]
    
        var body: some View {
            HStack {
                VStack(alignment: .leading) {
                    ForEach(labels, id: \.self) { label in
                        Text(label)
                            .frame(maxHeight: .infinity)
                            .padding(.bottom, 4)
                    }
                }
    
                VStack {
                    ForEach(labels, id: \.self) { label in
                        TextField(label, text: self.$text)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                    }
                }
                .padding(.leading)
            }
            .padding(.horizontal)
            .fixedSize(horizontal: false, vertical: true)
        }
    }
    

    Here is the resulting preview:

    in order to account for the misaligned baselines of the external and internal labels (a collateral issue that is not related to this specific layout – see for instance this discussion) I manually added the padding

    credits to this website for enlightening me on the path to understanding SwiftUI layout trickeries

    0 讨论(0)
  • 2021-02-04 09:40
        HStack{
            Image(model.image)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 10, alignment: .leading)
            VStack(alignment: .leading) {
                Text("Second column ")
                Text("Second column -")
            }
            Spacer()
            Text("3rd column")
        }
    

    1- first column - image

    2- second column - two text

    3- the float value

    Spacer() - Play with Spacer() -> above example image and vstack remains together vertical align for all rows, just put spacer for the views you want to do in another vertical alignment / column VStack(alignment: .leading. - this is importent to make alignment start

    0 讨论(0)
  • 2021-02-04 09:42

    Looks like this will work:

    extension HorizontalAlignment {
        private enum MyAlignment: AlignmentID {
            static func defaultValue(in context: ViewDimensions) -> Length {
                context[.trailing]
            }
        }
        static let myAlignmentGuide = HorizontalAlignment(MyAlignment.self)
    }
    
    struct ContentView : View {
        @State var username: String = ""
        @State var email: String = ""
        @State var password: String = ""
    
        var body: some View {
            VStack(alignment: .myAlignmentGuide) {
                HStack {
                    Text("Username").alignmentGuide(.myAlignmentGuide, computeValue: { d in d[.trailing] })
                    TextField($username)
                        .textFieldStyle(.roundedBorder)
                        .frame(maxWidth: 200)
                }
                HStack {
                    Text("Email")
                        .alignmentGuide(.myAlignmentGuide, computeValue: { d in d[.trailing] })
                    TextField($email)
                        .textFieldStyle(.roundedBorder)
                        .frame(maxWidth: 200)
                }
                HStack {
                    Text("Password")
                        .alignmentGuide(.myAlignmentGuide, computeValue: { d in d[.trailing] })
                    TextField($password)
                        .textFieldStyle(.roundedBorder)
                        .frame(maxWidth: 200)
                }
            }
        }
    }
    

    With that code, I am able to achieve this layout:

    The caveat here is that I had to specify a max width for the TextFields. Left unconstrained, the layout system described in the WWDC talk I linked in the comments retrieves a size for the TextField prior to alignment happening, causing the TextField for email to extend past the end of the other two. I'm not sure how to address this in a way that will allow the TextFields to expand to the size of the containing view without going over...

    0 讨论(0)
  • 2021-02-04 09:47

    You need to add fixed width and leading alignment. I've tested in Xcode 11.1 it's ok.

    struct TextInputWithLabelDemo: View {
        @State var text = ""
        let labels = ["Username", "Email", "Password"]
    
        var body: some View {
            VStack {
                ForEach(labels, id: \.self) { label in
                    HStack {
                        Text(label).frame(width: 100, alignment: .leading)
                        TextField(label, text: self.$text)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                    }
                }
                .padding(.horizontal)
                .fixedSize(horizontal: false, vertical: true)
            }
        }
    }
    

    Below You can see what the issue when we use different VStack for Text and TextField. See more info here

    Updated 16 Oct 2019

    A closer inspection of Texts and TextFields you can notice that they have different heights and it effects the positions of Texts relative to TextFields as you can see on the right side of the screenshot that Password Text is higher relative to Password TextField than the Username Text relative to Username TextField. I gave three ways to resolve this issue here

    0 讨论(0)
  • 2021-02-04 09:51

    You could use kontiki's geometry reader hack for this:

    struct Column: View {
        @State private var height: CGFloat = 0
        @State var text = ""
        let spacing: CGFloat = 8
    
        var body: some View {
            HStack {
                VStack(alignment: .leading, spacing: spacing) {
                    Group {
                        Text("Hello world")
                        Text("Hello Two")
                        Text("Hello Three")
                    }.frame(height: height)
                }.fixedSize(horizontal: true, vertical: false)
                VStack(spacing: spacing) {
                    TextField("label", text: $text).bindHeight(to: $height)
                    TextField("label 2", text: $text)
                    TextField("label 3", text: $text)
                }.textFieldStyle(RoundedBorderTextFieldStyle())
            }.fixedSize().padding()
        }
    }
    
    extension View {
        func bindHeight(to binding: Binding<CGFloat>) -> some View {
            func spacer(with geometry: GeometryProxy) -> some View {
                DispatchQueue.main.async { binding.value = geometry.size.height }
                return Spacer()
            }
            return background(GeometryReader(content: spacer))
        }
    }
    

    We are only reading the height of the first TextField here and applying it three times on the three different Text Views, assuming that all TextFields have the same height. If your three TextFields have different heights or have appearing/disappearing verification labels that affect the individual heights, you can use the same technique but with three different height bindings instead.

    Why is this a bit of a hack?

    Because this solution will always first render the TextFields without the labels. During this render phase it will set the height of the Text labels and trigger another render. It would be more ideal to render everything in one layout phase.

    0 讨论(0)
提交回复
热议问题