Is it possible to resize text with matched geometry effect?

Viewed 497

I have an overlay transition that uses matchedgeometry effect, as shown in this video: Matched Geometry Effect with same sized text

I want to enlarge the transitioned text after the animation, but it seems that the text frame isn't being scaled in time, causing the text to be shortened in transition. Matched Geometry Effect with different sized text

Is there a way to overcome this?

The original text:

Text(info.name)
    .font(.system(size: 22, weight: .bold))
    .matchedGeometryEffect(id: info.name, in: namespace)

The transitioned text:

Text(currentCard.name)
    .font(.title.bold())
    .matchedGeometryEffect(id: currentCard.name, in: namespace)

Thanks

2 Answers

I understand text is not animating with matchedGeometryEffect (macOS 12 / Xcode 13). I could not find a way to do it. Either the matchedGeo is working, or an AnimatableModifier on the font, but not both at the same time.

For some use cases you can work around it by forcing the container sizes of the original text and the container size of the transitioned text staying equal with a .frame modifier. This has the effect that there are at least no ellipses ("...") showing during animation:

struct ContentView: View {
    @State private var showHero = false
    @Namespace var ns_hero
    
    var body: some View {
        VStack {
            if showHero == false {
                VStack(alignment: .center) {
           
                    Spacer()
                    
                    VStack {
                        Image("Red")
                            .resizable()
                            .scaledToFit()
                            .matchedGeometryEffect(id: "image", in: ns_hero)
                            .frame(maxWidth: 200, maxHeight: 200)
                        
                        Text("Red")
                            .font(.body)
                            .frame(maxWidth: 300, maxHeight: 50)
                            .matchedGeometryEffect(id: "title", in: ns_hero)
                    }
                    .onTapGesture {
                        withAnimation {
                            showHero.toggle()
                        }
                    }
                }
            } else {
                VStack(alignment: .center) {
                    
                    VStack {
                        Image("Red")
                            .resizable()
                            .scaledToFit()
                            .matchedGeometryEffect(id: "image", in: ns_hero)
                            .frame(maxWidth: 400, maxHeight: 400)
                        
                        Text("Red Rectangle")
                            .font(.largeTitle)
                            .frame(maxWidth: 300, maxHeight: 50)
                            .matchedGeometryEffect(id: "title", in: ns_hero)
                    }
                    .onTapGesture {
                        withAnimation {
                            showHero.toggle()
                        }
                    }
                    
                    Spacer()
                }
            }
        }
    }
}

enter image description here

enter image description here

enter image description here

Finally figured it out!

Animating font size inside matchedGeometryEffect

Add a separate font size @State var that you trigger inside your matchedGeometryEffect's if statement .onAppear. Combine this with the AnimatableCustomFontModifier and the font size will animate just fine!

I know. It sounds gross. Here's a working example.

struct ContentView: View {
    
    @Namespace var namespace
    @State var hero = false
    @State var heroFontLarge = false
    

    var body: some View {
        VStack {
            if hero {
                Text("hello")
                    .animatableFont(name: "San Francisco", size: heroFontLarge ? 64 : 16)
                    .matchedGeometryEffect(id: "title", in: namespace)
                    // Start the font size animation. Only gets called once the transition starts
                    .onAppear {
                        withAnimation(.linear(duration: 1)) {
                            heroFontLarge = true // Start animating a larger font
                        }
                    }
                    .transition(.scale(scale: 1)) // Stops a fading out bug
            } else {
                Text("hello")
                    .animatableFont(name: "San Francisco", size: heroFontLarge ? 64 : 16)
                    .matchedGeometryEffect(id: "title", in: namespace)
                    // Start the font size animation. Only gets called once the transition starts
                    .onAppear {
                        withAnimation(.linear(duration: 1)) {
                            heroFontLarge = false  // Start animating a smaller font
                        }
                    }
                    .transition(.scale(scale: 1)) // Stops a fading out bug
                
            }
            Button("Toggle") {
                withAnimation(.linear(duration: 1)) {
                    hero.toggle()
                }
            }
        }
    }
}



// A modifier that animates a font through various sizes.
// https://www.hackingwithswift.com/quick-start/swiftui/how-to-animate-the-size-of-text
struct AnimatableCustomFontModifier: ViewModifier, Animatable {
    var name: String
    var size: Double

    var animatableData: Double {
        get { size }
        set { size = newValue }
    }

    func body(content: Content) -> some View {
        content
            .font(.custom(name, size: size))
    }
}

// To make that easier to use, I recommend wrapping
// it in a `View` extension, like this:
extension View {
    func animatableFont(name: String, size: Double) -> some View {
        self.modifier(AnimatableCustomFontModifier(name: name, size: size))
    }
}

Unfortunately, it's a little buggy if you try to reverse the animation before it's finished. Should't be a problem if you can delay interaction until the animation is finished.

Related