How to apply the same font size to multiple Texts within an HStack and size the HStack to fit its parent?

180 views Asked by At

I have an HStack with three Texts:

HStack {
    Text("Hey")
    Text(".")
    Text("What's up?")
}

All I want is the HStack to fill the available space of its parent container under the constraint that all three Texts have the same font size in the end.

The following naive approach obviously doesn't work:

struct ThreeTextsView: View {
    var body: some View {
        HStack {
            Text("Hey")
            Text(".")
            Text("What's up?")
        }
        .font(.system(size: 500)) // set a crazy-height font-size
        .minimumScaleFactor(0.01) // so it can be scaled down
        .frame(width: 200, height: 200) // ← this only simulates the size of the container
    }
}

This is the output:

Rendered view

Each Text view is scaled individually according to the preferences applied to the whole HStack, so we end up with three different font sizes in the same HStack. Of course, I could use a fixed font size instead, but then the compound of the three texts would not dynamically fill its container when the container is resized.

How can I solve this (without magic numbers and additional assumptions)?

The three Texts must be baseline-aligned in the end. (That's clearly not the case in the example above.)

Note: John Sundell has written a great article on how to give views an equal width or height based on their intrinsic (ideal) sizes. However, my problem is a little different as it also requires knowledge of the outside world (the container's size), so it has one additional constraint.

2

There are 2 answers

1
timbre timbre On

I see many ways to solve this, but concentrating on requirement to have the same font size for all text entities, the simplest is to use Text(_:format:) like this:

struct ThreeTextsView: View {
    var values = [ // <-- all the text parts are just strings
        "Hey",
        ".",
        "What's up?"
    ]
    var body: some View {
        HStack {
            Text(values, format: SimpleJoin()) // <-- we display them in one Text element, 
                                               // so their style is consistent 
        }
        .font(.system(size: 500)) 
        .minimumScaleFactor(0.01) 
        .frame(width: 200, height: 200)
    }
}

and the SimpleJoin is just

struct SimpleJoin: FormatStyle {
    func format(_ value: [String]) -> String {
        value.joined()
    }
}

this solution also supports setting a line limit to 1 for the entire text (which will reduce the font):

Text(values, format: SimpleJoin())
    .lineLimit(1)

enter image description here

0
Benzy Neez On

Turns out, there is an easy solution. You just need to add .scaledToFit... but it must come before the frame! The modifier minimumScaleFactor is still needed too:

var body: some View {
    HStack {
        Text("Hey")
        Text(".")
        Text("What's up?")
    }
    .font(.system(size: 500)) // set a crazy-height font-size
    .minimumScaleFactor(0.01) // so it can be scaled down
    .scaledToFit() // MUST BE BEFORE FRAME!
    .frame(width: 200, height: 200) // ← this only simulates the size of the container
}

ScaledToFit


EDIT

Actually, if you look closely you will notice that the height of the third block is slightly less than the first block - they are not perfectly aligned.

It works better if you use zero spacing on the HSTack and then include Text spacers between the blocks (if you need spacing at all):

HStack(alignment: .firstTextBaseline, spacing: 0) {
    Text("Hey")
    Text("  ")
    Text(".")
    Text("  ")
    Text("What's up?")
}
.font(.system(size: 50)) // set a crazy-height font-size
.minimumScaleFactor(0.01) // so it can be scaled down
.scaledToFit() // MUST BE BEFORE FRAME!
.frame(width: 200, height: 200) // ← this only simulates the size of the container

ZeroSpacing