Segmented Picker with onTapGesture doesn't respond to taps
Asked Answered
P

4

9

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.

Pretrice answered 30/7, 2019 at 12:46 Comment(0)
T
6

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.

Tomy answered 30/7, 2019 at 15:58 Comment(5)
I was planning on doing something similar but simpler. The second picker in my code works if I take away pickerStyle() so it is something in the segmented code that isn't working.Pretrice
It may be that the code for a segmented control has different gesture-related code because the view needs to know where within the view the user tapped, while in a default picker the location of the tap within the view doesn't need to be known. We don't have the source code for SwiftUI so there is no way to know exactly why the tap gesture for a segmented control picker style's tap gesture recognition has lower precedence than a manually appended tap gesture, but that is not the case for a default picker.Tomy
And to further illustrate that this is an issue related to gesture precedence, if you add a tapGesture to a default picker, the picker responds properly to your touch, but the tapGesture that you manually added is not registered. In that case, the picker's gesture recognition has precedence over the one you manually added. Without access to SwiftUI source code, I cannot definitively tell you why that's the case.Tomy
Thanks for your time. My solution was to create a wrapper ObservableObject class for Int that calls a closure when it changes value. Now I don't need to worry about taps.Pretrice
@MichaelSalmon Yeah no problem, that's the route my answer suggested you take. If it was helpful and thorough, could you accept the answer?Tomy
I
3
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)")
        }
    }
}
Intern answered 21/12, 2019 at 15:9 Comment(1)
Absolute answer for me.Haggai
P
2

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
                }
            }
Pesticide answered 3/11, 2020 at 6:27 Comment(1)
This was the easiest solution to implement. Because it seems the picker's built in tap gesture is being interfered with; you're basically correcting that interference by explicitly defining how the picker should work. Thanks!Thegn
I
0

.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()) {}
Irremeable answered 1/9, 2023 at 4:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.