I have 2 views in SwiftUI in the HStack. I want to change their places, so 1. view jumps to 2. position and 2. to 1. position.
Here is some code example:
HStack {
Spacer()
LeftChannelView()
.offset(x: swapLeftToRight ? calculatedOffset : 0.0)
Spacer()
RightChannelView()
.offset(x: swapLeftToRight ? -calculatedOffset : 0.0)
Spacer()
}
only calculatedOffset
is unknown. I don't want to hardcode the value. I also don't want to calculate from UIScreen.bounds
.
How should I calculate calculatedOffset
?
Setting offset
seems to work and also is animated nicely when I toggle swapLeftToRight
.
withAnimation {
swapLeftToRight.toggle()
}
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.