Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI Font scaling in a complex View

Tags:

ios

swift

swiftui

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?

like image 592
wk.experimental Avatar asked Oct 20 '19 16:10

wk.experimental


2 Answers

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)
    }
like image 74
X.Creates Avatar answered Sep 29 '22 03:09

X.Creates


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

like image 28
George Avatar answered Sep 29 '22 04:09

George