Search code examples
swiftswiftuiswiftui-animation

SwiftUI swap 2 views's places (once replaces the other)


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

Solution

  • 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.