1

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 :

GIF showing the result of the expandable picker

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)

2 Answers 2

2

Nice job. You can add an .offset() to the contents of the ScollView, which shifts it left depending on the selection:

enter image description here

        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()
        }
Sign up to request clarification or add additional context in comments.

3 Comments

It works perfectly thank you ! How can I do if each item has a different width ? I need to use GeometryReader right ?
yes, either GeometryReader or alignmentGuide
it was challenging me, so I looked for another option. look at my second answer :)
1

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.

enter image description here

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()
                }
            }
    }
    
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.