How can I recognise a 'second' tap on a segmented control in SwiftUI?

414 Views Asked by At

In SwiftUI, I have a Picker with a .segmented style that I use to select how a list is sorted. What I want is for each segment of that picker to reverse the sort order, in addition to its normal behaviour.
Below is an example of a list with a segmented control in the header that is used to change how such a list can be sorted:

struct ContentView: View {
    @State private var animals = [
        Animal(name: "Lion", number: 1),
        Animal(name: "Monkey", number: 2),
        Animal(name: "Elephant", number: 3),
    ]
    @State private var sortOrder: SortOrder = .alphabetical

    var body: some View {
        List {
            Section {
                ForEach(animals) { animal in
                    HStack {
                        Text(animal.name)
                        Spacer()
                        Text("\(animal.number)")
                    }
                }
            } header: {
                VStack(alignment: .leading, spacing: 0) {
                    Text("Sort:")
                    HStack {
                        Picker("Sorting methods", selection: $sortOrder) {
                            ForEach(SortOrder.allCases, id: \.self) { sortOrder in
                                Text(sortOrder.description)
                            }
                        }
                        .pickerStyle(.segmented)
                    }
                }
                .onChange(of: sortOrder) { sortOrder in
                    withAnimation {
                        sort(by: sortOrder)
                    }
                }
                .onAppear() {
                    sort(by: self.sortOrder)
                }
            }
        }
        .listStyle(.inset)
    }

    private func sort(by sortOrder: SortOrder) {
        switch sortOrder {
            case .alphabetical: return animals.sort(by: { $0.name < $1.name })
            case .numerical:    return animals.sort(by: { $0.number < $1.number })
        }
    }
}

struct Animal: Identifiable {
    var id = UUID()
    var name: String
    var number: Int
}

enum SortOrder: CaseIterable {
    case alphabetical
    case numerical

    var description: String {
        switch self {
            case .alphabetical: return "Alphabetical"
            case .numerical:    return "Numerical"
        }
    }
}

This all works fine: the list gets sorted by name or by number. What I want now is to reverse the sort order by tapping on a selected segment again. So for example, by tapping on segment 'alphanumeric' for the first time, the list is sorted A-Z. I want a second tap on that same segment to sort Z-A. Similar for numeric sorting.
Unfortunately, I don't get any notification that a segment is tapped for a second time. I tried using .onTapGesture, but that a) interferes with the normal picker behaviour and b) only specifies that there was a tap somewhere on the picker, but not which segment was tapped.
I guess I could make my own custom picker, but I am trying to avoid that, because SwiftUI's picker does a great job otherwise. Except for the second tap, that is. :-).
Any ideas/suggestions are highly appreciated. Thanks!

1

There are 1 best solutions below

2
On

try something like this, with a double tap on the picker buttons:

struct ContentView: View {
    @State private var animals = [
        Animal(name: "Lion", number: 1),
        Animal(name: "Monkey", number: 2),
        Animal(name: "Elephant", number: 3),
    ]
    @State var sortOrder: SortOrder = .alphabetical
    @State var sortReverse = false  // <-- here
    
    var body: some View {
        List {
            Section {
                ForEach(animals) { animal in
                    HStack {
                        Text(animal.name)
                        Spacer()
                        Text("\(animal.number)")
                    }
                }
            } header: {
                VStack(alignment: .leading, spacing: 0) {
                    Text("Sort:")
                    HStack {
                        Picker("Sorting methods", selection: $sortOrder) {
                            ForEach(SortOrder.allCases, id: \.self) { sortOrder in
                                Text(sortOrder.description)
                            }
                        }
                        .onTapGesture(count: 2) { // <-- here
                            sortReverse.toggle()
                            sort(by: sortOrder)
                        }
                        .pickerStyle(.segmented)
                    }
                }
                .onChange(of: sortOrder) { sortOrder in
                    withAnimation {
                        sortReverse = false  // <-- here
                        sort(by: sortOrder)
                    }
                }
                .onAppear() {
                    sort(by: sortOrder)
                }
            }
        }
        .listStyle(.inset)
    }

    // -- here
    private func sort(by sortOrder: SortOrder) {
        switch sortOrder {
        case .alphabetical: animals.sort(by: sortReverse ? { $0.name > $1.name } : { $0.name < $1.name } )
        case .numerical:    animals.sort(by: sortReverse ? { $0.number > $1.number } : { $0.number < $1.number })
        }
    }
    
}