0

I am trying to recreate a segmented picker with customized buttons. My goal is that when switching the tab, the background smoothly transitions from the former active tab to the new active tab. it mostly works, but when I am trying to switch back (eg. from tab3 to tab2 or tab1), the animation is gone/ or does not work. Am I missing something?

struct CustomSegmentedPickerView: View {

private var titles = ["products", "causes", "info"]
private var colors = [Color.primaryAccent, Color.primaryAccent, Color.primaryAccent]
@State private var currentIndex: Int = 0
@Namespace var namespace
@Namespace var namespace2



var body: some View {
    VStack (alignment: .center){
        ZStack {
            HStack {
                ForEach (0 ..< titles.count) {index in
                    Button {
                        withAnimation(.default){
                            self.currentIndex = index
                        }
                    } label: {
                    ZStack {
                        if index == currentIndex {
                            Rectangle()
                                .frame(height: 40)
                                .cornerRadius(770)
                                .foregroundColor(
                                    self.colors[self.currentIndex].opacity(0.3))
                                .matchedGeometryEffect(id: "background", in: namespace)
                            
                        } else {
                                Rectangle()
                                    .frame(height: 40)
                                    .cornerRadius(770)
                                    .matchedGeometryEffect(id: "background2", in: self.namespace2)
                                    .foregroundColor(.clear)
                        }
                            Text(self.titles[index])
                                .foregroundColor(.black)
                        }
                    }
                }
            }
            .padding()
        }
    }
 }
}

I thought maybe I need a new namespace ID, but it does not change anything. Any help is appreciated. Thanks in advance!

BR

kai
  • 3
  • 2

1 Answers1

0

I tried your example and it actually seemed to be working ok (using an iPhone 14 simulator running iOS 16.4 with Xcode 14.3), except that the labels themselves would flash between selections. However, the following warning/error is being reported in the console:

Multiple inserted views in matched geometry group Pair(first: "background2", second: SwiftUI.Namespace.ID(id: 84)) have `isSource: true`, results are undefined.

I noticed you were also using an enormous corner radius on the background, but I don't think it could be the reason for the issue you were describing.

So I have to admit, I am not familiar with .matchedGeometryEffect. But if it's difficult to get it working, it sounds like a good reason to try a simpler solution.

I would suggest, a simpler way to get the background to move between the items is just to use two layers, one for the background and one for the labels, then adjust the x-offset of the background in accordance with the selection. The following works as I think you want it to:

struct CustomSegmentedPickerView: View {

    private let titles = ["products", "causes", "info"]
    private let colors = [Color.green, Color.blue, Color.red]
    @State private var currentIndex: Int = 0

    /// - Returns the width of a picker item
    private func itemWidth(availableWidth: CGFloat) -> CGFloat {
        availableWidth / CGFloat(titles.count)
    }

    /// - Returns the x-offset for the current selection
    private func xOffsetForSelection(availableWidth: CGFloat) -> CGFloat {
        itemWidth(availableWidth: availableWidth) * CGFloat(currentIndex)
    }

    var body: some View {
        GeometryReader { proxy in
            ZStack(alignment: .leading) {

                // The background that moves between the items
                RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(colors[currentIndex].opacity(0.3))
                    .frame(
                        width: itemWidth(availableWidth: proxy.size.width),
                        height: proxy.size.height
                    )
                    .offset(x: xOffsetForSelection(availableWidth: proxy.size.width))

                // The labels for the items
                HStack {
                    ForEach(Array(titles.enumerated()), id: \.element) { i, element in
                        Text("\(element)")
                            .frame(maxWidth: .infinity)
                            .onTapGesture {
                                withAnimation { currentIndex = i }
                            }
                    }
                }
            }
        }
        .padding(.horizontal)
        .frame(height: 40)
    }
}
Benzy Neez
  • 1,546
  • 2
  • 3
  • 10