1

I need to calculate the upper bound for the maximum font size, at which the text with the given font fits on one line.

In order to calculate the upper bound for the font size picker, I need to calculate the width of the Text. And I am stuck here. How can I calculate the width of the Text? Please help.

If there's another solution to my problem, I would this appreciate as well.

enter image description here

Here's my code:

extension Text {
    var width: CGFloat {
        return 0 // MARK: Calculate Text width somehow?
    }
}

struct EditView: View {
    @State private var string: String = "Hello, world!"
    @State private var fontSize: CGFloat = 20
    @State private var fontWeight: Font.Weight = .regular
    @State private var fontWidth: Font.Width = .standard
    @State private var fontDesign: Font.Design = .default
    
    let squareWidth: CGFloat = 150
    
    var font: Font {
        Font.system(size: fontSize, weight: fontWeight, design: fontDesign).width(fontWidth)
    }
    var fontSizeRange: ClosedRange<CGFloat>  {
        let upperBound = maximumFontSizeThatFitsOnOneLineFor(string: string, with: font, in: squareWidth)
        return 10...upperBound
    }
    
    func maximumFontSizeThatFitsOnOneLineFor(string: String, with font: Font, in maximumTextWidth: CGFloat) -> CGFloat {
        var fontSize: CGFloat = 100
        var text = Text(string).font(font)
        var textWidth: CGFloat = text.width

        while textWidth > maximumTextWidth {
            fontSize -= 1
        }
        return fontSize
    }
        
    var body: some View {
        VStack {
            HStack {
                Spacer()
                AnotherView(string: string, font: font)
                        .frame(maxWidth: squareWidth, maxHeight: squareWidth)
                        .clipShape(RoundedRectangle(cornerRadius: 20))
                Spacer()
            }
            .padding()

            Form {
                TextField("Enter text", text: $string)
                Stepper("Font Size: \(fontSize.formatted())", value: $fontSize, in: fontSizeRange)
                Picker("Weight", selection: $fontWeight) {
                    Text("Ultra Light").tag(Font.Weight.ultraLight)
                    Text("Regular").tag(Font.Weight.regular)
                    Text("Heavy").tag(Font.Weight.heavy)
                }
                Picker("Width", selection: $fontWidth) {
                    Text("Compressed").tag(Font.Width.compressed)
                    Text("Standard").tag(Font.Width.standard)
                    Text("Expanded").tag(Font.Width.expanded)
                }
                Picker("Design", selection: $fontDesign) {
                    Text("Default").tag(Font.Design.default)
                    Text("Monospaced").tag(Font.Design.monospaced)
                    Text("Serif").tag(Font.Design.serif)
                }
            }
        }
    }
}

struct AnotherView: View {
    var string: String
    var font: Font
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(string)
                    .lineLimit(1)
                    .font(font)
                Spacer(minLength: 0)
                Text("")
                    .font(.largeTitle)
            }
            Spacer(minLength: 0)
        }
        .foregroundColor(.white)
        .padding(16)
        .background { Color.red }

    }
}

struct EditView_Previews: PreviewProvider {
    static var previews: some View {
        EditView()
    }
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
user14119170
  • 1,191
  • 3
  • 8
  • 21
  • 1
    Just found [this](https://stackoverflow.com/a/58782429/16855023) maybe it helps. – photangralenphie Mar 30 '23 at 20:10
  • Thanks. I tried it, but it seems like this gives incorrect width sometimes (at lest in terms of SwiftUI's Text's width), for example for Width: .compressed & Weight: .black it returns quite significantly wrong result. It seems like there is no perfect solution to this problem right now. – user14119170 Mar 31 '23 at 09:23

2 Answers2

1

You could try something like this. Use a GeometryReader on a clear background of the text. It is a bit hacky but works:

@State var textWidth: CGFloat = 0
@State private var fontSize: CGFloat = 20

var body: some View {
    Text("My Text")
        .font(.system(size: fontSize))
        .background {
            GeometryReader { proxy in
                Color.clear
                    .onAppear {
                        textWidth = proxy.size.width
                    }
                    .onChange(of: fontSize) { _ in
                        textWidth = proxy.size.width
                    }
            }
        }
}
  • Thank you for the reply. I will try to figure out whether it's possible to apply this to my code somehow. – user14119170 Mar 30 '23 at 16:37
  • I have tried. It seems to be impossible to use this solution with the while loop. Therefore it seems to be impossible to use this solution to set the upperBound for the font size picker. Thanks for the effort anyway – user14119170 Mar 30 '23 at 19:51
0

You can just slap on a large font size, then let Swift scale it down to the largest size that fits:

    Text(string)
        .lineLimit(1)
        .allowsTightening(true) // <-- ADDED
        .minimumScaleFactor(0.1) // <-- ADDED
        .font(font)
Benzy Neez
  • 1,546
  • 2
  • 3
  • 10