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.
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)
}
}