swiftuitextlayoutviewhstack

Align vertical center of View with first line of multiline Text within an HStack


I am attempting to achieve a layout within an HStack that contains two views: A Text view that may or may not contain multiple lines of text, and a Button. These are aligned with the top edge of the containing HStack.

For the purpose of this question, the Button could be any kind of View.

I want the height of the Button to be equal to the height of the first line of the text contained within the Text element.

This is easy when the Text contains a single line - there are obvious ways to make two views equally tall. But it's unclear to me how to do this when the Text contains multiple lines of text when the size of the text could differ based on accessibility settings, etc.

I know how to do this in UIKit, but am unsure of how to do it with SwiftUI. Does anybody have any ideas? Thanks!

I've included a screenshot showing more or less what I'm going for. In this example I've hardcoded the height of the Button (so it looks pretty close), but this won't work if the Text's font size changes for any reason.

HStack containing two views: A multiline Text and a Button. Both are vertically aligned.

Edit 1: What I've Tried

Not a whole lot. I think the solution hinges on being able to get the metrics of the font associated with the text displayed by the Text, but it's not clear how to do so in SwiftUI. This font could come from anywhere, be derived from a Font.TextStyle, and is subject to the user's Dynamic Type setting.

My searching for this has turned up nothing so far, hence my question here.

Edit 2: Code Sample

Below is code from my simplified hardcoded implementation. Here I (roughly) estimate the size of a .title3 TextStyle to manually set the height of the Button, but this is intended to be a generalized solution. So, when this is put in production the font could be any size (either derived from a TextStyle or via some sort of system/custom font).

HStack(alignment: .top, spacing: 4.0, content: {
    Text(title)
    .font(.title3)
    .fontWeight(.medium)
    .frame(maxWidth: .infinity, alignment: .leading)
    .border(Color.gray)

    Button(action: {
    }, label: {
        Text("See More")
        .font(.caption)
    })
    .frame(minHeight: 26.0, alignment: .center)
    .border(Color.gray)
})

Solution

  • This can be solved by using a hidden placeholder to establish the footprint for the button.

    struct ContentView: View {
        let title = "Distinctio eligendi maxime non officia aut ratione deliti. Et aliquid maiores adipisci."
    
        private var theButton: some View {
            Button("See More") {}
                .font(.caption)
        }
    
        private var buttonFootprint: some View {
            ZStack {
                Text("X")
                    .font(.title3)
                    .fontWeight(.medium)
                theButton
                    .disabled(true)
            }
            .hidden()
        }
    
        var body: some View {
            HStack(alignment: .top) {
                Text(title)
                    .font(.title3)
                    .fontWeight(.medium)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .border(.gray)
                buttonFootprint
                    .overlay {
                        theButton
                            .frame(maxHeight: .infinity)
                            .border(.gray)
                    }
            }
        }
    }
    

    Screenshot

    An alternative approach would be to compute the height of 1 line of text using the lineHeight delivered by a UIFont. A ScaledMetric can be used to adapt to dynamic font sizes, my answer to How do I have achieve a combined line limit for two Text views in a VStack in SwiftUI? shows how.