iosswiftswiftuipicker

Segmented picker in iOS - handle tap on already selected item


I use segmented picker in iOS which contains few items. When user tap on not selected item this item becomes selected. Some of items can contain sub items. So when user tap on already selected type I need to show modal window with subitems for choosing one of them. But taps on already selected items of segmented picker are not handling. I tried to use "long press" but it doesn't work as well.

I would like to use native iOS design that's why I don't want use "buttons" instead segmented picker.

So the question is how I can handle tap on already selected item of segmented picker for showing sub items to choosing one of them? It can be "long press" or other alternative which will be intuitive for user.

import SwiftUI

struct CustomSegmentedPicker: View {
    
    @State private var showModalSelectD: Bool = false
    
    enum periods {
        case A, B, C, D, All
    }
    
    @State var predefinedPeriod: periods = periods.All
    @State var predefinedPeriodD: String = "D1"
    
    var body: some View {
        ZStack {
            Color.clear
                .sheet(isPresented: $showModalSelectD, content: {
                    List {
                        Picker("D", selection: $predefinedPeriodD) {
                            Text("D1").tag("D1")
                            Text("D2").tag("D2")
                            Text("D3").tag("D3")
                        }
                        .pickerStyle(.inline)
                    }
                })
            VStack {
                HStack {
                    Picker("Please choose a currency", selection: $predefinedPeriod) {
                        Text("A").tag(periods.A)
                        Text("B").tag(periods.B)
                        Text("C").tag(periods.C)
                        Text("D (\(predefinedPeriodD))").tag(periods.D)
                            .contentShape(Rectangle())
                            .simultaneousGesture(LongPressGesture().onEnded { _ in
                                print("Got Long Press")
                                showModalSelectD.toggle()
                            })
                            .simultaneousGesture(TapGesture().onEnded{
                                print("Got Tap")
                                showModalSelectD.toggle()
                            })
                        Text("All").tag(periods.All)
                    }
                    .pickerStyle(SegmentedPickerStyle())
                }
            }
        }
    }
}

Solution

  • You could take advantage of the fact that the buttons of a segmented picker all have equal widths. An overlay can be used to cover just the picker item that is currently active. The overlay can then intercept further tap gestures on the same item. If a different item is selected, the overlay moves to that one instead, etc.

    The code below uses the technique of .matchedGeometryEffect. This requires a namespace:

    @Namespace private var ns
    

    To make the implementation a little simpler, I also made the enum CaseIterable:

    enum periods: CaseIterable {
        case A, B, C, D, All
    }
    

    The overlay is then applied like this:

    Picker("Please choose a currency", selection: $predefinedPeriod) {
        Text("A").tag(periods.A)
        Text("B").tag(periods.B)
        Text("C").tag(periods.C)
        Text("D (\(predefinedPeriodD))").tag(periods.D)
        Text("All").tag(periods.All)
    }
    .pickerStyle(SegmentedPickerStyle())
    .background {
    
        // A row of placeholders
        HStack(spacing: 0) {
            ForEach(periods.allCases, id: \.self) { period in
                Color.clear
                    .matchedGeometryEffect(id: period, in: ns, isSource: true)
            }
        }
    }
    .overlay {
        Rectangle()
            .fill(.background)
            .opacity(0.001)
            .matchedGeometryEffect(id: predefinedPeriod, in: ns, isSource: false)
            .simultaneousGesture(LongPressGesture().onEnded { _ in
                print("Got Long Press on \(predefinedPeriod)")
                if predefinedPeriod == .D {
                    showModalSelectD.toggle()
                }
            })
            .simultaneousGesture(TapGesture().onEnded{
                print("Got Tap on \(predefinedPeriod)")
                if predefinedPeriod == .D {
                    showModalSelectD.toggle()
                }
            })
    }