Assuring Same Font Size in Separately Resizing SwiftUI Text() Views.

Very excited to find this „tutorial“ SwiftUI equal scaling text size to fit by George Garside. I’ve been looking for a solution for this for some time. So I took it for a spin in Swift Playgrounds.

I put some Color rectangles left and right to constrain space and add a slider to be able to adjust the space on the fly.

Looks like it mostly does it’s job, but…

  • Sometimes the lines aren’t aligned properly to the leading edge.
  • Once more space becomes available the font size doesn’t increase to use the space available.

There is an easier solution to his problem though: just replace all spaces with newlines, count the lines you have and then just use one multiline Text() view. Like so:

struct MySolution: View {
    let text: String
    func formatForMultiline(_ text: String) -> String {
        text.replacingOccurrences(of: " ", with: "\r")
    }
    func howManyTextLines(_ text: String) -> Int {
        text.components(separatedBy: " ").count
    }
    var body: some View {
        Text(formatForMultiline(formatForMultiline(text)))
            .font(.largeTitle)
            .fontWeight(.bold)
            .lineLimit(howManyTextLines(text))
            .minimumScaleFactor(0.1)
    }
}

Now I’m thinking: there has to be a way to make two Text() views same dynamic font size if you cannot connect them that also gives back free space when it becomes available. Maybe it PreferenceKey needs to know there is more space?

Anyway… Here’s my complete Playground for now in case you want to mess around with it…

import SwiftUI
import PlaygroundSupport

struct WordsToFit: View {
    let text: String
    private struct SizePreferenceKey: PreferenceKey {
        static var defaultValue: CGFloat = .zero
        static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
            value = min(value, nextValue())
        }
    }
    
    @State private var wordHeight: CGFloat = 100
    
    var body: some View {
        VStack(alignment: .leading) {
            ForEach(text.split(separator: " ").map(String.init), id: \.self) { (word: String) in
                    Text(word)
                        .font(.largeTitle)
                        .fontWeight(.bold)
                        .scaledToFit()
                        .minimumScaleFactor(0.01)
                        .lineLimit(1)
                        .background(GeometryReader {
                            Color.clear.preference(key: SizePreferenceKey.self, value: $0.size.height)
                        })
                        .frame(maxHeight: wordHeight)
                }
            }
            .onPreferenceChange(SizePreferenceKey.self, perform: { wordHeight = $0 })
    }
}

struct MySolution: View {
    let text: String
    func formatForMultiline(_ text: String) -> String {
        text.replacingOccurrences(of: " ", with: "\r")
    }
    func howManyTextLines(_ text: String) -> Int {
        text.components(separatedBy: " ").count
    }
    var body: some View {
        Text(formatForMultiline(formatForMultiline(text)))
            .font(.largeTitle)
            .fontWeight(.bold)
            .lineLimit(howManyTextLines(text))
            .minimumScaleFactor(0.1)
    }
}

struct ContentView: View {
    @State var rectWidth: Double = 25
    let theText = "Testing nonbreaking space, verylongword."
    var body: some View {
        Slider(value: $rectWidth, in: 0.0...500.0) {
            Text("\(rectWidth)")
        }
        HStack { 
            Color.red.frame(width: CGFloat(rectWidth), height: 50)
            WordsToFit(text: theText)
            Color.blue.frame(width: CGFloat(rectWidth), height: 50)
        }
        HStack { 
            Color.red.frame(width: CGFloat(rectWidth), height: 50)
            MySolution(text: theText)
            Color.green.frame(width: CGFloat(rectWidth), height: 50)
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())