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