Michael Salmon
Michael Salmon

Reputation: 1184

Segmented Picker with onTapGesture doesn't respond to taps

I tried to reimplement the SegmentedControlers that I was using as they became deprecated in Xcode 11 beta 5. It took a while but I got the look I wanted. However when I replaced the tapAction with an onTapGesture() then the picker stopped working.

The code below shows the problem. Commenting out the pickerStyle gets a wheel picker which does work with onTapGesture()

import SwiftUI

var oneSelected = false
struct ContentView: View {
    @State var sel = 0
    var body: some View {
        VStack {
            Picker("Test", selection: $sel) {
                Text("A").tag(0)
                Text("B").tag(1)
                Text("C").tag(2)
            }
            .pickerStyle(SegmentedPickerStyle())
            Picker("Test", selection: $sel) {
                Text("A").tag(0)
                Text("B").tag(1)
                Text("C").tag(2)
            }
            .pickerStyle(SegmentedPickerStyle())
            .onTapGesture(perform: {
                oneSelected = (self.sel == 1)
            })
            Text("Selected: \(sel)")
        }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

I expect that Picker().pickerStyle(SegmentedPickerStyle()) should work the same way as SegmentedController() did.

Upvotes: 9

Views: 6258

Answers (4)

Steve Ham
Steve Ham

Reputation: 3154

import SwiftUI
import Combine

class IndexManager: ObservableObject {
    @Published var index = 0 {
        didSet {
            publisher.send(index)
        }
    }
    let publisher = PassthroughSubject<Int, Never>()
}

struct SegmentedPickerView: View {
    
    private let strings = ["a", "b", "c"]
    @ObservedObject private var indexManager = IndexManager()
    
    var body: some View {
        Picker("", selection: $indexManager.index) {
            ForEach(strings, id: \.self) {
                Text($0).tag(strings.firstIndex(of: $0)!)
            }
        }
        .pickerStyle(SegmentedPickerStyle())
        .onReceive(indexManager.publisher) { int in
            print("onReceive \(int)")
        }
    }
}

Upvotes: 3

Daggerpov
Daggerpov

Reputation: 364

.onTapGesture() does not seem to work with a Picker with a .pickerStyle attribute value of SegmentedPickerStyle().

An alternative that worked for me (taken from this answer is using the .onReceive() call as such:

                Picker("", selection: $quoteFrequencyIndex) {
                    ForEach(0..<frequencyOptions.count, id: \.self) { index in
                        Text(self.frequencyOptions[index])
                            .foregroundColor(colorPalettes[safe: colorPaletteIndex]?[1] ?? .white)
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
                .onReceive([self.quoteFrequencyIndex].publisher.first()) { _ in
                    WidgetCenter.shared.reloadTimelines(ofKind: "QuoteDropletWidget")
                }

Abstracted to a general template, where you can put your own logic in the onReceive() call, controlled by your selection variable:

Picker(selection: ${variable}){}
.pickerStyle(SegmentedPickerStyle())
.onReceive([self.{variable}].publisher.first()) {}

Upvotes: 0

Di Nerd Apps
Di Nerd Apps

Reputation: 818

i was able to get this working with the following condition in a onTapGesture

@State private var profileSegmentIndex = 0    

Picker(selection: self.$profileSegmentIndex, label: Text("Music")) {
                    Text("My Posts").tag(0)
                
                Text("Favorites").tag(1)
            }
            .onTapGesture {
                if self.profileSegmentIndex == 0 {
                    self.profileSegmentIndex = 1
                } else {
                    self.profileSegmentIndex = 0
                }
            }

Upvotes: 2

RPatel99
RPatel99

Reputation: 8096

The tapGesture you added interferes with the picker's built in tap gesture recognizing, which is why the code in your .onTapGesture runs when the picker is tapped, but the picker itself is not responding to taps. In your case, I suggest a different approach: pass a view model that conforms to ObservableObject into your ContentView, and have it contain an @Published variable for the picker selection. Then add a property observer to that variable that checks if the selected option is 1.

For example:

class ViewModel: ObservableObject {
    @Published var sel = 0 {
        didSet {
            oneSelected = oldValue == 1
        }
    }
    var oneSelected = false
}

In SceneDelegate.swift, or wherever ContentView is declared:

ContentView().environmentObject(ViewModel())

In ContentView.swift:

@EnvironmentObject var env: ViewModel
var body: some View {
    VStack {
        Picker("Test", selection: $env.sel) {
            Text("A").tag(0)
            Text("B").tag(1)
            Text("C").tag(2)
        }
        .pickerStyle(SegmentedPickerStyle())
        Picker("Test", selection: $env.sel) {
            Text("A").tag(0)
            Text("B").tag(1)
            Text("C").tag(2)
        }
        .pickerStyle(SegmentedPickerStyle())
        Text("Selected: \(sel)")
    }
}

Note: In my experience, adding a tapGesture to a SegmentedControl in previous betas resulted in the SegmentedControl being unresponsive, so I'm not sure why it was working for you in previous version. As of SwiftUI beta 5, I don't think there is a way to assign priority levels to gestures.

Edit: You can use .highPriorityGesture() to make your gesture take precedence over gestures defined in the view, but your gesture having higher precedence is causing your problem. You can, however, use .simultaneousGesture(), which I thought would be the solution to your problem, but I don't think it is fully functional as of SwiftUI Beta 5.

Upvotes: 6

Related Questions