I’m trying to create a custom ‘tab’ selection control with a horizontal row of options and the user can select one of N number of options. The ‘selected’ option will have a ‘border’ around it. Here’s a prototype I made:
@objc public enum ContactTabStyle: Int, CaseIterable {
case one, two, three, four
public var segmentTitle: String {
switch self {
case .one: return "Hello"
case .two: return "World"
case .three: return "Three"
case .four: return "Four"
}
}
}
struct SwiftUIView: View {
let segments: [ContactTabStyle] = [.one, .two, .three, .four]
@State var selectedTab: ContactTabStyle = .one
@Namespace var tabName
var body: some View {
HStack {
ForEach(segments, id: \.self) { segment in
Button {
selectedTab = segment
} label: {
Text(segment.segmentTitle)
.padding(12.0)
.border(selectedTab == segment ? Color.blue : Color.clear, width: 3.0)
.cornerRadius(4.0)
.matchedGeometryEffect(id: segment.segmentTitle, in: tabName) // doesn't work
}
}
}
}
}
The view looks and works fine, but I can’t get the animation to ‘slide’ from one selection to another. It just does a normal SwiftUI fade-in-and-out. I believe I should use matchedGeometryEffect
to get the sliding effect, but it doesn’t seem to be working. I’ve tried adding the matchedGeometryEffect
to the label around the button as well, but it didn’t work either.
Here’s a preview of what it looks like:
The Text
s don’t need to match geometry – it’s the borders that need to match geometry.
If you use border
to make a border, the border is not its own “view”, so you can’t modify only the border with matchedGeometryEffect
. One workaround is to add the border as the background
of either the Text
or the Button
(these produce slightly different effects – see which you like better).
Button {
selectedTab = segment
} label: {
Text(segment.segmentTitle)
.padding(12.0)
.background {
if selectedTab == segment {
RoundedRectangle(cornerRadius: 4)
.stroke(lineWidth: 3)
// every border should have the same id!
.matchedGeometryEffect(id: "selection", in: tabName)
}
}
}
or
Button {
selectedTab = segment
} label: {
Text(segment.segmentTitle)
.padding(12.0)
}
.background {
if selectedTab == segment {
RoundedRectangle(cornerRadius: 4)
.stroke(Color.accentColor, style: .init(lineWidth: 3))
.matchedGeometryEffect(id: "selection", in: tabName)
}
}
.animation(.default, value: selectedTab)