skip to Main Content

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


  1. 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

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search