Matched geometry effect along a path in SwiftUI

219 Views Asked by At

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

1

There are 1 best solutions below

0
On BEST ANSWER

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!

struct ContentView: View {

    enum Box: Hashable {
        case blue
        case red
        case yellow
    }

    @State private var target: Box = .blue
    @Namespace private var namespace

    private func colorForBox(box: Box) -> Color {
        let result: Color
        switch box {
        case .blue: result = .blue
        case .red: result = .red
        case .yellow: result = .yellow
        }
        return result
    }

    private func box(_ box: Box, size: CGFloat) -> some View {
        colorForBox(box: box)
            .frame(width: size, height: size)
            .matchedGeometryEffect(id: box, in: namespace, isSource: target == box)
    }

    private func switchPosition() {
        let newTarget: Box
        let via: Box
        switch target {
        case .blue:
            via = .yellow
            newTarget = .red
        case .yellow:
            via = .red
            newTarget = .blue
        case .red:
            via = .blue
            newTarget = .yellow
        }
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
            withAnimation(.easeInOut(duration: 1)) {
                target = newTarget
            }
        }
        withAnimation(.easeInOut(duration: 1)) {
            target = via
        }
    }

    var body: some View {
        VStack(spacing: 40) {
            ZStack {
                Color.clear
            }
            .overlay(alignment: .top) { box(.blue, size: 100) }
            .overlay(alignment: .bottomLeading) { box(.yellow, size: 80) }
            .overlay(alignment: .trailing) { box(.red, size: 175) }
            .overlay {
                Color.gray
                    .opacity(0.5)
                    .border(.gray)
                    .matchedGeometryEffect(id: target, in: namespace, isSource: false)
            }
            .frame(height: 450)
            .frame(maxWidth: .infinity)
            .padding()

            Button("Switch position", action: switchPosition)
                .buttonStyle(.borderedProminent)
        }
    }
}

CurvedPath