Search code examples
swiftuiuisegmentedcontrol

Disable a segment in a SwiftUI SegmentedPickerStyle Picker?


My SwiftUI app has a segmented Picker and I want to be able to disable one or more options depending on availability of options retrieved from a network call. The View code looks something like:

    @State private var profileMetricSelection: Int = 0
    private var profileMetrics: [RVStreamMetric] = [.speed, .heartRate, .cadence, .power, .altitude]
    @State private var metricDisabled = [true, true, true, true, true]

    var body: some View {
        VStack(alignment: .leading, spacing: 2.0) {
            ...(some views)...
            Picker(selection: $profileMetricSelection, label: Text("")) {
                ForEach(0 ..< profileMetrics.count) { index in
                    Text(self.profileMetrics[index].shortName).tag(index)
                }
            }.pickerStyle(SegmentedPickerStyle())
            ...(some more views)...
        }
    }

What I want to be able to do is modify the metricDisabled array based on network data so the view redraws enabling the relevant segments. In UIKit this can be done by calls to setEnabled(_:forSegmentAt:) on the UISegmentedControl but I can't find a way of doing this with the SwiftUI Picker

I know I can resort to wrapping a UISegmentedControl in a UIViewRepresentable but before that I just wanted to check I'm not missing something...


Solution

  • you can use this simple trick

    import SwiftUI
    
    struct ContentView: View {
        @State var selection = 0
        let data = [1, 2, 3, 4, 5]
        let disabled = [2, 3] // at index 2, 3
        var body: some View {
    
            let binding = Binding<Int>(get: {
                self.selection
            }) { (i) in
                if self.disabled.contains(i) {} else {
                    self.selection = i
                }
            }
    
            return VStack {
                Picker(selection: binding, label: Text("label")) {
                    ForEach(0 ..< data.count) { (i) in
                        Text("\(self.data[i])")
                    }
                }.pickerStyle(SegmentedPickerStyle())
                Spacer()
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    enter image description here

    Maybe something like

    ForEach(0 ..< data.count) { (i) in
        if !self.disabled.contains(i) {
            Text("\(self.data[i])")
        } else {
            Spacer()
        }
    }
    

    could help to visualize it better

    enter image description here

    NOTES (based on the discussion)

    From user perspective, the Picker is one control, which could be in disabled / enabled state.

    The option selected from Picker is not control, it is some value. If you make a list of controls presented to the user, some of them could be disabled, just to inform the user, that the action associated with it is not currently available (like menu, some buttons collection etc.)

    I suggest you to show in Picker only values which could be selected. This collection of values could be updated any time.

    UPDATE

    Do you like something like this?

    enter image description here

    No problem at all ... (copy - paste - try - modify ...)

    import SwiftUI
    
    struct Data: Identifiable {
        let id: Int
        let value: Int
        var disabled: Bool
    }
    
    struct ContentView: View {
        @State var selection = -1
        @State var data = [Data(id: 0, value: 10, disabled: true), Data(id: 1, value: 20, disabled: true), Data(id: 2, value: 3, disabled: true), Data(id: 3, value: 4, disabled: true), Data(id: 4, value: 5, disabled: true)]
        var filteredData: [Data] {
            data.filter({ (item) -> Bool in
                item.disabled == false
            })
        }
        var body: some View {
            VStack {
                VStack(alignment: .leading, spacing: 0) {
                    Text("Select from avaialable")
                        .padding(.horizontal)
                        .padding(.top)
                    HStack {
                        GeometryReader { proxy in
                            Picker(selection: self.$selection, label: Text("label")) {
                                ForEach(self.filteredData) { (item) in
                                    Text("\(item.value.description)").tag(item.id)
                                }
                            }
                            .pickerStyle(SegmentedPickerStyle())
                            .frame(width: CGFloat(self.filteredData.count) * proxy.size.width / CGFloat(self.data.count), alignment: .topLeading)
    
                            Spacer()
                        }.frame(height: 40)
                    }.padding()
                }.background(Color.yellow.opacity(0.2)).cornerRadius(20)
                Button(action: {
                    (0 ..< self.data.count).forEach { (i) in
                        self.data[i].disabled = false
                    }
                }) {
                    Text("Enable all")
                }
                Button(action: {
                    self.data[self.selection].disabled = true
                    self.selection = -1
                }) {
                    Text("Disable selected")
                }.disabled(selection < 0)
                Spacer()
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }