Interpolating corner radius with matched geometry effect

In the following snippet, it's obvious that matched geometry effect is overlaying both views and crossfading between them. Is there a way to evenly animate the corner radius change?

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

 var body: some View {
  VStack(spacing: 16) {
   Toggle("Toggle", isOn: $toggled.animation(.linear(duration: 5)))

   if toggled {
    RoundedRectangle(cornerRadius: 24, style: .continuous)
     .fill(.red)
     .matchedGeometryEffect(id: "box", in: namespace)
     .frame(width: 200, height: 100)

    Spacer()
   } else {
    Spacer()

    RoundedRectangle(cornerRadius: 12, style: .continuous)
     .fill(.blue)
     .matchedGeometryEffect(id: "box", in: namespace)
     .frame(width: 100, height: 200)
   }
  }
  .padding(24)
 }
}

Replies

Yes, there is: don't use two RoundedRectangles. 🙂

Instead, use one and mutate the values that you want to differ. Here's how I implemented it in your code:

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

    var body: some View {
        VStack(spacing: 16) {
            Toggle("Toggle", isOn: $toggled.animation(.linear(duration: 30)))

            if toggled {
                Spacer()
            }

            RoundedRectangle(cornerRadius: toggled ? 24 : 12, style: .continuous)
                .fill(toggled ? .red : .blue)
                .matchedGeometryEffect(id: "box", in: namespace)
                .frame(width: toggled ? 200 : 100, height: toggled ? 100 : 200)

            if !toggled {
                Spacer()
            }

        }
        .padding(24)

    }
}

How it works:

  • toggled ? value1 : value2 returns value1 if toggled is true or value2 if toggled is false. This works for anything as long as both values are of the same type.
  • @State tells SwiftUI to update the view any time that toggled changes.

So any time you flip the switch, it toggles toggled which forces a recalculation of the view with new values for cornerRadius, .fill, width, and height, and after the recalculation, animates the changes according to your .animation parameters.

I think this gives precisely the kind of animation that you were looking for.

So any time you want to do something like this, instead of having two views that you switch between, use one view and mutate its values.

  • One more thing: my code says .linear(duration: 30)) instead of 5. I changed it on my copy to see the animation more clearly. Don't forget to change it back in your project.

  • Definitely makes sense if the two views are siblings! The reason why I'm trying to do this with matched geometry effect is because the source and destination views may be in different view hierarchies (possibly different screens that we're transitioning between) but we also want to minimize coupling between them and avoid coordinating state at some higher parent view.

Add a Comment