I'm trying to create an expandable segmented picker in SwiftUI, I've done this so far :
struct CustomSegmentedPicker: View {
@Binding var preselectedIndex: Int
@State var isExpanded = false
var options: [String]
let color = Color.orange
var body: some View {
HStack {
ScrollView(.horizontal) {
HStack(spacing: 4) {
ForEach(options.indices, id:\.self) { index in
let isSelected = preselectedIndex == index
ZStack {
Rectangle()
.fill(isSelected ? color : .white)
.cornerRadius(30)
.padding(5)
.onTapGesture {
preselectedIndex = index
withAnimation(.easeInOut(duration: 0.5)) {
isExpanded.toggle()
}
}
}
.shadow(color: Color(UIColor.lightGray), radius: 2)
.overlay(
Text(options[index])
.fontWeight(isSelected ? .bold : .regular)
.foregroundColor(isSelected ? .white : .black)
)
.frame(width: 80)
}
}
}
.transition(.move(edge: .trailing))
.frame(width: isExpanded ? 80 : CGFloat(options.count) * 80 10, height: 50)
.background(Color(UIColor.cyan))
.cornerRadius(30)
.clipped()
Spacer()
}
}
}
Which gives this result :
Now, when it contracts, how can I keep showing the item selected and hide the others ? (for the moment, the item on the left is always shown when not expanded)
CodePudding user response:
Nice job. You can add an .offset()
to the contents of the ScollView
, which shifts it left depending on the selection:
HStack {
ScrollView(.horizontal) {
HStack(spacing: 4) {
ForEach(options.indices, id:\.self) { index in
let isSelected = preselectedIndex == index
ZStack {
Rectangle()
.fill(isSelected ? color : .white)
.cornerRadius(30)
.padding(5)
.onTapGesture {
preselectedIndex = index
withAnimation(.easeInOut(duration: 0.5)) {
isExpanded.toggle()
}
}
}
.shadow(color: Color(UIColor.lightGray), radius: 2)
.overlay(
Text(options[index])
.fontWeight(isSelected ? .bold : .regular)
.foregroundColor(isSelected ? .white : .black)
)
.frame(width: 80)
}
}
.offset(x: isExpanded ? CGFloat(-84 * preselectedIndex) : 0) // <<< here
}
.transition(.move(edge: .trailing))
.frame(width: isExpanded ? 80 : CGFloat(options.count) * 80 10, height: 50)
.background(Color(UIColor.cyan))
.cornerRadius(30)
.clipped()
Spacer()
}
CodePudding user response:
Here is another approach using .matchedGeometryEffect
, which can handle different label widths without falling back to GeometryReader
.
Based on expansionState it either draws only the selected item or all of them and .matchedGeometryEffect
makes sure the animation goes smooth.
struct CustomSegmentedPicker: View {
@Binding var preselectedIndex: Int
@State var isExpanded = false
var options: [String]
let color = Color.orange
@Namespace var nspace
var body: some View {
HStack {
HStack(spacing: 8) {
if isExpanded == false { // show only selected option
optionLabel(index: preselectedIndex)
.id(preselectedIndex)
.matchedGeometryEffect(id: preselectedIndex, in: nspace, isSource: true)
} else { // show all options
ForEach(options.indices, id:\.self) { index in
optionLabel(index: index)
.id(index)
.matchedGeometryEffect(id: index, in: nspace, isSource: true)
}
}
}
.padding(5)
.background(Color(UIColor.cyan))
.cornerRadius(30)
Spacer()
}
}
func optionLabel(index: Int) -> some View {
let isSelected = preselectedIndex == index
return Text(options[index])
.fontWeight(isSelected ? .bold : .regular)
.foregroundColor(isSelected ? .white : .black)
.padding(8)
.background {
Rectangle()
.fill(isSelected ? color : .white)
.cornerRadius(30)
}
.onTapGesture {
preselectedIndex = index
withAnimation(.easeInOut(duration: 0.5)) {
isExpanded.toggle()
}
}
}
}