0

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.

Mischa
  • 15,816
  • 8
  • 59
  • 117
  • have you tried custom layouts via the Layout protocol? – iSpain17 Aug 29 '23 at 15:48
  • No. Didn't know about those until a minute ago... would that be a good tool for the job? Will look into them. – Mischa Aug 29 '23 at 15:50
  • If you were to set a ```lineLimit``` of 1 then it could be achieved quite easily using ```Text``` concatenation. But if the three text blocks can wrap independently then it's more complicated... – Benzy Neez Aug 29 '23 at 16:10
  • In my specific use case, I actually want one-liners. However, I need to use an `HStack` because I want to align the entire thing with respect to the dot (second `Text`) in the end. I guess it's hard to base a layout guide on a character position in a concatenated string... :-/ – Mischa Aug 29 '23 at 17:00

2 Answers2

0

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

timbre timbre
  • 12,648
  • 10
  • 46
  • 77
  • Thanks for this nice idea! Unfortunately, it does not answer the question as the base assumption is that I have three `Texts` in an `HStack`. (I need separate `Texts` for alignment reasons as explained in more detail in a comment on the question above.) – Mischa Aug 29 '23 at 18:48
0

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

Benzy Neez
  • 1,546
  • 2
  • 3
  • 10