Search code examples
swiftswiftuiswiftui-animation

matchedGeometryEffect not always animates the position changes


My goal is to simulate a pawn that jumps from square to square.

struct MyView: View {
    @State var current = 0;
    @State var colors : [Color] = [.blue, .gray, .red]
    @Namespace var animationNamespace : Namespace.ID
    var body : some View {
        HStack(spacing: 12){
            ForEach(colors.indices) { i in
                ZStack{
                    RoundedRectangle(cornerRadius: 8)
                        .fill(colors[i])
                        .frame(width: 50, height: 50)
                    Image(systemName: "person.crop.square")
                        .resizable()
                        .scaledToFit()
                        .cornerRadius(8)
                        .opacity(current == i ? 1.0 : 0.0)
                        .frame(width: 50, height: 50)
                        .matchedGeometryEffect(id: current == i ? -1 : i , in: animationNamespace)
                        .onTapGesture {
                            withAnimation(.easeInOut){
                                current = (current + 1) % colors.capacity
                            }
                        }
                }
            }
        }
    }
}

animations

click 1 - from position 0 to position 1 : OK
click 2 - from position 1 to position 2 : OK
click 3 - from position 3 to position 0 : KO
click 4 - from position 0 to position 1 : OK
click 5 - from position 1 to position 2 : OK
click 6 - from position 3 to position 0 : KO
click 7 - from position 0 to position 1 : OK
...

https://developer.apple.com/documentation/swiftui/view/matchedgeometryeffect(id:in:properties:anchor:issource:)

If inserting a view in the same transaction that another view with the same key is removed, the system will interpolate their frame rectangles in window space to make it appear that there is a single view moving from its old position to its new position.

Am I missing something?
Are there any limitations?

Xcode Version 12.1 (12A7403) iOS 14.0


Solution

  • Here is a slight modification that makes it work. Instead of adding the image to all 3 squares with different opacities, only draw the image on the one that currently contains the pawn. Do this using if current == i { }. If you do this, then you can just use 1 as the matchedGeometryEffect id.

    struct ContentView: View {
        @State var current = 0;
        @State var colors : [Color] = [.blue, .gray, .red]
        @Namespace var animationNamespace : Namespace.ID
        var body : some View {
            HStack(spacing: 12){
                ForEach(colors.indices) { i in
                    ZStack{
                        RoundedRectangle(cornerRadius: 8)
                            .fill(colors[i])
                            .frame(width: 50, height: 50)
                        if current == i {
                            Image(systemName: "person.crop.square")
                                .resizable()
                                .scaledToFit()
                                .cornerRadius(8)
                                .frame(width: 50, height: 50)
                                .matchedGeometryEffect(id: 1 , in: animationNamespace)
                                .onTapGesture {
                                    withAnimation(.easeInOut){
                                        current = (current + 1) % colors.capacity
                                    }
                                }
                        }
                    }
                }
            }
        }
    }
    

    Here it is in the simulator:

    simulator demo