skip to Main Content

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


  1. 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()
            }
    
    Login or Signup to reply.
  2. 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()
                    }
                }
        }
        
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search