1
votes

I'm trying to learn SwiftUI and I read this really great article about alignmentGuides and how to align views that are in different containers: https://swiftui-lab.com/alignment-guides/

In my UI, I want to have a checkmark, then a Text() label question on one line (orange), then below that I want the TextField() where they can answer the question (green), and I want the TextField()'s leading to match up with Text() question's leading. Like so:

example of what I want

The above screenshot was generated when I forcibly set the width of the green TextField() with .frame(width: 200). I don't want to force the textfield to be a certain size, but when I

Unfortunately, whenever I remove the .frame(width: 200)...

1) the green TextField() then stretches wider than the screen 2) the outer VStack stretches beyond the screen as well. 3) and the checkmark Image() in the orange HStack is aligned off of the leading side of the screen.

Here is what it looks like without the TextField().frame(width: 200), notice the blue outline of the VStack is off both sides of the screen:

screenshot of the problem layout

Here is the code:

extension HorizontalAlignment {
    private enum QuestionAlignment : AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            return d[.leading]
        }
    }

    static let questionAlignment = HorizontalAlignment(QuestionAlignment.self)
}

struct EasyModeQuestion: View {

    @Binding var field : String

    var body: some View {
        VStack(alignment: .questionAlignment) {
            Rectangle()
                .fill(Color.primary)
                .frame(width: 1)
                .alignmentGuide(.questionAlignment, computeValue: { d in d[HorizontalAlignment.leading] })

            HStack() {
                Image(systemName: "checkmark.square")

                Text("What is the blah blah blah?")
                    .alignmentGuide(.questionAlignment, computeValue: { d in d[.leading] })

            }.background(Color.orange)


            TextFieldRoundedRect(placeholder: "", text: $field)
                .alignmentGuide(.questionAlignment, computeValue: { d in d[.leading] })
                .background(Color.green)
                //.frame(width: 200)


            Rectangle()
                .fill(Color.primary)
                .frame(width: 1)
                .alignmentGuide(.questionAlignment, computeValue: { d in d[HorizontalAlignment.leading] })
        }

    }
}

The two Rectangle() objects above and below are examples from the link at the top of my question, which are supposed to help visualize where the alignment guide is sitting.

When I just go with a basic leading alignment and remove all the alignment guides, then the TextField() returns to an appropriate width:

enter image description here

2

2 Answers

1
votes

I'm the author of the article you reference at the beginning of your question. Alignments do come with their challenges, don't they?. In your case, let me propose a different approach. I know that to many, this one could look cheap... but it accomplishes a very important point: code simplicity. I'm aware your question may be a simplification just to expose the problem you're dealing, and if that is the case, then the solution provided may not help... but here it is in case it does: Just use a spacer. I can think of two simple ways of doing it. One quicker but dirtier, and the other a little more elegant, but requires a few more lines of code:

enter image description here

#1 Quick and Dirty: Use an invisible checkmark image as a spacer.

struct ContentView: View {
    @State private var text = ""

    var body: some View {
        EasyModeQuestion(field: self.$text)
            .frame(width: 400)
    }
}

struct EasyModeQuestion: View {

    @Binding var field : String

    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Image(systemName: "checkmark.square")

                Text("What is the blah blah blah?")

            }

            HStack {
                Image(systemName: "checkmark.square").opacity(0.0)

                TextField("", text: $field)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
            }

        }

    }
}

#2 Nicer spacer?: Use the width of the image for a Color.clear view.

struct ContentView: View {
    @State private var text = ""

    var body: some View {
        EasyModeQuestion(field: self.$text)
            .frame(width: 400)
    }
}

struct EasyModeQuestion: View {

    @Binding var field : String

    var body: some View {
        let img = UIImage(systemName: "checkmark.square")!

        return VStack(alignment: .leading) {
            HStack {
                Image(uiImage: img)

                Text("What is the blah blah blah?")

            }

            HStack {
                Color.clear.frame(width: img.size.width, height: 0)

                TextField("", text: $field)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
            }
        }
    }
}
0
votes

SwiftUI performs layout differently that UIKit AutoLayout

  1. Make subviews calculate all intrinsic content sizes
  2. Perform layout for them

So alignment is applied after TextField calculates its size, but TextField does not have own size, so it grabs all possible width proposed by parent view, ie. screen-wide. Then when alignment is applied everything get mess.

So you have somehow limit the size of TextField to value where you want (btw, which one - you did not specify?)

Case 1: hardcode - as you did with .frame(width: 200)

Case 2: fixed size to placeholder

TextFieldRoundedRect(placeholder: "your answer is here", text: $field)
    .alignmentGuide(.questionAlignment, computeValue: { d in d[.leading] })
    .background(Color.green)
    .fixedSize()

demo

Case 3: calculate dynamically (GeometryReader, AnchorPreference, etc.) to the point you need