One way to achieve this is by carefully applying matchedGeometryEffect
twice to each swappable view (so four times in total).
import PlaygroundSupport
import SwiftUI
struct SwappyView: View {
@Binding var swapped: Bool
@Namespace var namespace
var body: some View {
HStack {
Spacer()
Text("Lefty Loosey")
.matchedGeometryEffect(
id: swapped ? "right" : "left",
in: namespace,
properties: .position,
anchor: .center,
isSource: false
)
.matchedGeometryEffect(
id: "left",
in: namespace,
properties: .position,
anchor: .center,
isSource: true
)
Spacer()
Text("Righty Tighty")
.matchedGeometryEffect(
id: swapped ? "left" : "right",
in: namespace,
properties: .position,
anchor: .center,
isSource: false
)
.matchedGeometryEffect(
id: "right",
in: namespace,
properties: .position,
anchor: .center,
isSource: true
)
Spacer()
}
}
}
struct DemoView: View {
@State var swapped: Bool = false
var body: some View {
VStack {
SwappyView(swapped: $swapped)
Button("Swap!") {
withAnimation {
swapped.toggle()
}
}
}
}
}
PlaygroundPage.current.setLiveView(DemoView())
The reason this works may be difficult to understand. The use of matchedGeometryEffect(..., isSource: false)
, can reposition the modified view, but that repositioning happens after the view’s layout has been computed. (The offset
and position
modifiers also work this way.) So the frames captured by the matchedGeometryEffect(..., isSource: true)
modifiers are the frames where the views would appear if there were no isSource: false
modifiers.