animationswiftuipickermatchedgeometryeffect

Animation for Custom Segmented Picker in SwiftUI


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


Solution

  • EDIT The question was about using .matchedGeometryEffect but the (accepted) answer I provided before did not use that technique. I have now replaced the solution in the answer, to show how .matchedGeometryEffect can be used, as this is really the better approach.


    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.
    

    To fix, I would suggest the following changes:

    Other changes:

    struct CustomSegmentedPickerView: View {
        private var titles = ["products", "causes", "info"]
    //    private var colors = [Color.primaryAccent, Color.primaryAccent, Color.primaryAccent]
        private let colors = [Color.green, Color.blue, Color.red]
        @State private var currentIndex: Int = 0
        @Namespace var namespace
    
        var body: some View {
            HStack {
                ForEach(Array(titles.enumerated()), id: \.offset) { index, title in
                    Button(title) {
                        currentIndex = index
                    }
                    .foregroundStyle(.black)
                    .frame(maxWidth: .infinity, minHeight: 40)
                    .matchedGeometryEffect(id: index, in: namespace, isSource: true)
                }
            }
            .background {
                Capsule()
                    .fill(colors[currentIndex].opacity(0.3))
                    .matchedGeometryEffect(id: currentIndex, in: namespace, isSource: false)
            }
            .animation(.default, value: currentIndex)
            .padding()
        }
    }
    

    Animation