Search code examples
iosswiftanimationswiftui

Is there a more elegant way to pull off a regular to bold text animation in SwiftUI


I have a UI who's design calls for a button that when pressed becomes bold.

Since my real view is very constrained I need to have the text layout using its bold version and then overlay it with the regular version of the font.

Text("Brat")
    .fontWeight(.bold)
    .foregroundStyle(Color.black.opacity(isSelected ? 1 : 0))
    .overlay(alignment: .center) {
        Text("Brat")
            .fontWeight(.regular)
            .opacity(isSelected ? 0 : 1)
    }
    .padding()
    .id(model.title)

This works surprisingly well honestly. And if this is the best that can be done then so be it.

But, is there a way to do this better? Is there some way to get deep enough that you can set a float value to the weight and have TextKit animate between the two weights directly?

Of note solution would need to work in cases where the text does need to wrap to a second line. This does mean the per character solution is out of the window unless I want to implement word wrap myself.

I am more than happy to go into UIKit or Core Animation etc to pull this off. I just need an overall plan for how I might leverage those to do better here.

In the video notice how especially the "B" does not transition perfectly smoothly.

The text "Brat" animating from bold to regular


Solution

  • If I understand correctly, you need to determine the size of the view using the bold form. The text in regular font should use the same footprint.

    The bold form that determines the footprint does not need to be visible, it can remain hidden. Then the visible version can be shown as an overlay, with the appropriate font weight.

    Btw, the modifier .contentTransition lets you apply different kinds of transition to the text change. But the default in this case seems to be .interpolate, which is what you want anyway. So this modifier is not needed.

    struct ContentView: View {
        @State private var isSelected = false
        let text = "The quick brown fox jumps over the lazy dog"
    
        var body: some View {
            Text(text)
                .fontWeight(.bold)
                .hidden()
                .overlay {
                    Text(text)
                        .fontWeight(isSelected ? .bold : .regular)
                        // .contentTransition(.interpolate)
                }
                .padding()
                .background(.green, in: .rect(cornerRadius: 10))
                .onTapGesture {
                    withAnimation {
                        isSelected.toggle()
                    }
                }
                .padding()
        }
    }
    

    Animation


    EDIT When you have muilti-line text, there is still a possibility that the bold version will wrap differently to the regular version, which means you see the words moving around when the font weight changes. This problem existed with your earlier solution too.

    A way of mitigating this problem is to add a little horizontal padding to the regular version. The amount of padding will depend on the available width and on the font size. Testing on an iPhone 16 simulator with the example above, I found that padding of 10 works quite well. It also helps to make this a ScaledMetric, so that it adapts to dynamic text size.

    To safeguard against the padding causing the regular form to run out of space and then truncate, a minimumScaleFactor can also be applied. This way, it will try to squeeze into the space available, instead of truncating.

    @ScaledMetric private var regularPadding: CGFloat = 10.0
    
    .overlay {
        Text(text)
            .fontWeight(isSelected ? .bold : .regular)
            .padding(.horizontal, isSelected ? 0 : regularPadding)
            .minimumScaleFactor(0.8)
    }