0

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()
}
mackode
  • 73
  • 7
  • Does this answer your question? [Reverse order of stacked items](https://stackoverflow.com/questions/66230338/reverse-order-of-stacked-items) – Ranoiaetep Aug 31 '23 at 14:17
  • Unfortunately, `layoutDirection` breaks the `offset`, when I try to keep the layouted view interactive for later. It throws some `Layer` exception on dragging. IMO not a good solution though. – mackode Aug 31 '23 at 16:37

2 Answers2

1

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.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
0

You don't need to do any computations here, ever. On a simple level, the following code does exactly what you want:

struct SwapView: View {
    
    @State var leftChannelFirst = true
    
    var body: some View {
            ZStack {
                HStack {
                    Spacer()
                    LeftChannelView()
                    Spacer()
                    RightChannelView()
                    Spacer()
                }
                .opacity(leftChannelFirst ? 1 : 0)
                HStack {
                    Spacer()
                    RightChannelView()
                    Spacer()
                    LeftChannelView()
                    Spacer()
                }
                .opacity(leftChannelFirst ? 0 : 1)
            }
            .onTapGesture {
                withAnimation {
                    self.leftChannelFirst.toggle()
                }
            }
    }
}

You can add transitions, do matched geometry effects, or any number of advanced techniques. And, if you are worried that both "exist" on the screen at the same time, SwiftUI is highly optimized to not devote resources to invisible views.

Yrb
  • 8,103
  • 2
  • 14
  • 44
  • What if behind `Left` and `Right` `ChannelView` there is an `AVPlayer` instance that downloads resources (even if paused, and works only if attached to views hierachy)? I would like to create only one left channel and one right channel. – mackode Aug 31 '23 at 16:40