Home > Enterprise >  Expandable Custom Segmented Picker in SwiftUI
Expandable Custom Segmented Picker in SwiftUI

Time:01-15

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)

CodePudding user response:

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

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.

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