iosswiftswiftuiswiftui-matchedgeometryeffect

Is it possible to resize text with matched geometry effect?


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


Solution

  • 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. You wouldn't believe how long this took. 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.