swiftswiftuimatchedgeometryeffect

Matched geometry effect along a path in SwiftUI


Is it possible to have matched geometry effect follow a path? I noticed when you launch Netflix's mobile app and select a profile from the who's watching screen, the profile square scales up and centers, then it animates upwards and to the right into a mini profile square. This seems like a matched geometry effect but along a curved path instead of a straight line. I could not find anything in the matched geometry api apart from frame, size and position to achieve this effect.

enter image description here


Solution

  • It is possible to have a view move along a curved path if you change the source for the matchedGeometryEffect mid-way through the animation. You don't have much control over the exact path, but with some tweaks to timing it is possible to get it to curve quite nicely.

    It is important that the new target is applied before the first target is reached and withAnimation is used for all changes.

    Here is an example to show it working. It gets especially interesting when you press the button multiple times in succession!

    enum Box: Hashable {
        case blue
        case red
        case yellow
    
        var color: Color {
            switch self {
            case .blue: .blue
            case .red: .red
            case .yellow: .yellow
            }
        }
    
        var next: Box {
            switch self {
            case .blue: .red
            case .red: .yellow
            case .yellow: .blue
            }
        }
    
        var via: Box {
            switch self {
            case .blue: .yellow
            case .red: .blue
            case .yellow: .red
            }
        }
    }
    
    struct ContentView: View {
        @State private var target: Box = .blue
        @Namespace private var ns
    
        var body: some View {
            VStack(spacing: 40) {
                ZStack {
                    box(.blue, size: 100, alignment: .top)
                    box(.yellow, size: 80, alignment: .bottomLeading)
                    box(.red, size: 175, alignment: .trailing)
                }
                .overlay {
                    Color.gray
                        .opacity(0.5)
                        .border(.gray)
                        .matchedGeometryEffect(id: target, in: ns, isSource: false)
                }
                .frame(maxHeight: 450)
                .padding()
    
                Button("Switch position") {
                    let newTarget = target.next
                    let via = target.via
                    withAnimation(.easeInOut(duration: 1)) {
                        target = via
                    }
                    Task {
                        try? await Task.sleep(for: .seconds(0.5))
                        withAnimation(.easeInOut(duration: 1)) {
                            target = newTarget
                        }
                    }
                }
                .buttonStyle(.borderedProminent)
            }
        }
    
        private func box(_ box: Box, size: CGFloat, alignment: Alignment) -> some View {
            box.color
                .frame(width: size, height: size)
                .matchedGeometryEffect(id: box, in: ns, isSource: target == box)
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: alignment)
        }
    }
    

    CurvedPath