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
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:
.matchedGeometryEffect
. They each have a unique id (equal to the index of the button) and are flagged with isSource: true
.isSource: false
, which means it is matched to the source, instead of trying to be the source.currentIndex
as the id for .matchedGeometryEffect
, so when the selection changes it moves to the corresponding new source.Other changes:
Capsule
.Array(titles.enumerated())
..animation
modifier can be used instead of withAnimation
. It makes the code slightly more compact but otherwise works the same.ZStack
and VStack
were redundant -> removed..foregroundColor
is deprecated, use .foregroundStyle
instead.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()
}
}