10
votes

My goal is to make sure Text in a container to scale according to its parent. It works well when the container only contains one Text view, as following:

import SwiftUI

struct FontScalingExperiment: View {
    var body: some View {
        Text("Hello World ~!")
            .font(.system(size: 500))
            .minimumScaleFactor(0.01)
            .lineLimit(1)
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 20)
                    .fill(Color.yellow)
                    .scaledToFill()  
        )
    }
}

struct FontScalingExperiment_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            FontScalingExperiment()
                .previewLayout(.fixed(width: 100, height: 100))
            FontScalingExperiment()
                .previewLayout(.fixed(width: 200, height: 200))
            FontScalingExperiment()
                .previewLayout(.fixed(width: 300, height: 300))
            FontScalingExperiment()
                .previewLayout(.fixed(width: 400, height: 400))
        }

    }
}

the result:

Result

However, when we have more complex View, we cant use same approach to automatically scale the text based on its parent size, for example:

import SwiftUI

struct IndicatorExperiment: View {
    var body: some View {
        VStack {
            HStack {
                Text("Line 1")
                Spacer()
            }
            Spacer()
            VStack {
                Text("Line 2")
                Text("Line 3")
            }
            Spacer()
            Text("Line 4")
        }
        .padding()
        .background(
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.yellow)
        )
            .aspectRatio(1, contentMode: .fit)
    }
}

struct IndicatorExperiment_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            IndicatorExperiment()
                .previewLayout(.fixed(width: 100, height: 100))
            IndicatorExperiment()
                .previewLayout(.fixed(width: 200, height: 200))
            IndicatorExperiment()
                .previewLayout(.fixed(width: 300, height: 300))
            IndicatorExperiment()
                .previewLayout(.fixed(width: 400, height: 400))
        }
    }
}

Simply adding these 3 modifiers:

.font(.system(size: 500)) .minimumScaleFactor(0.01) .lineLimit(1)

wont produce result like the first example; Text enlarged beyond the frame. I did successfully, produce the result that I want by using GeometryReader then scale the font size based on geometry.size.width. Is this the only approach for achieving the desired result in SwiftUI?

2
I have the exact problem you have, and I cannot find an answer anywhere. All works as expected until I put views into an HStack. Do you mind sharing your solution with GeometryReader?Jeff
GeometryReader gives us the height and width of the frame, using this size we can set the font accordingly. for example: GeometryReader { g in HStack { ... } .font(.system(size: g.size.width / ratio)) } ratio is any number, to adjust the size. Need to do manual visual validation to make sure it render properly for your smallest frame.wk.experimental
Your solution works well. Visual inspection is the key. Thank you for sharing.Jeff

2 Answers

0
votes

Using GeometryReader and a .minimumScaleFactor modifier would probably the only way to scale text in a view. To have more control on sizing, one possible way is to provde the .frame size from the parent view.

Scalable Text View

    GeometryReader { geo in
        Text("Foo")
            .font(
                .system(size: min(geo.size.height, geo.size.width) * 0.95))
            .minimumScaleFactor(0.05)
            .lineLimit(1)
    }

Parent View that uses the Scalable Text View

    GeometryReader { geo in
        ScaleableText()
            .frame(width: geo.size.width, height: geo.size.height)
    }
0
votes

You can try make all the Texts the same height. To do this you will need to set the padding and spacing explicitly, so this will scale rather than the fixed default values.

Also, the Spacer() didn't make much sense here - if the requirement was that all the Text stay the same size, the Spacer would just make all the text small. For Text to scale based on space, and where Spacer tries to use as much space as possible, it's a contradiction. Instead, I decided to just set the VStack's spacing in the initializer.

Working code:

struct IndicatorExperiment: View {
    private let size: CGFloat
    private let padding: CGFloat
    private let primarySpacing: CGFloat
    private let secondarySpacing: CGFloat
    private let textHeight: CGFloat

    init(size: CGFloat) {
        self.size = size
        padding = size / 10
        primarySpacing = size / 15
        secondarySpacing = size / 40

        let totalHeights = size - padding * 2 - primarySpacing * 2 - secondarySpacing
        textHeight = totalHeights / 4
    }

    var body: some View {
        VStack(spacing: primarySpacing) {
            HStack {
                scaledText("Line 1")

                Spacer()
            }
            .frame(height: textHeight)

            VStack(spacing: secondarySpacing) {
                scaledText("Line 2")

                scaledText("Line 3")
            }
            .frame(height: textHeight * 2 + secondarySpacing)

            scaledText("Line 4")
        }
        .padding(padding)
        .background(
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.yellow)
        )
        .aspectRatio(1, contentMode: .fit)
        .frame(width: size, height: size)
    }

    private func scaledText(_ content: String) -> some View {
        Text(content)
            .font(.system(size: 500))
            .minimumScaleFactor(0.01)
            .lineLimit(1)
            .frame(height: textHeight)
    }
}

Code to test with:

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 50) {
                IndicatorExperiment(size: 100)

                IndicatorExperiment(size: 200)

                IndicatorExperiment(size: 300)

                IndicatorExperiment(size: 400)
            }
        }
    }
}

Result:

Result