4

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...

3
  • Not now. Use UISegmentedControl. Commented Mar 4, 2020 at 16:35
  • you better filer profileMetrics based on boolean flag (so don't show the option, if it is unavailable) Commented Mar 4, 2020 at 17:18
  • @Asperi you could mimic that, the only trouble is visual style :-( Commented Mar 4, 2020 at 17:41

2 Answers 2

6

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()
    }
}
Sign up to request clarification or add additional context in comments.

9 Comments

This violates Apple Human Interface Guidelines and behaves confusing. That's why I do not suggest such things (though, for the truth, I tried similar approach). So for now UISegmentedControl is preferable if segment disabling feature is needed.
@Asperi yes, we need all this xxxStyle protocols to be publicly available. or you can use spacer as temporary replacement in ForEach
Thanks both for the ideas, some imaginative thoughts there. It feels like a major update to SwiftUI is needed to bring the functionality closer to the level of UIKit, but I’m not sure if or when we will get that. In the meantime I think I will need to revert to UISegmentedControl to get it looking and behaving just right
@FleetPhil I personally will filter all "disabled" options out of picker. I like to see something is in "disabled state" only if user can enable it by some interaction, like temporary disabled login button till credentials are filled in etc. Disabled option in picker seem to be wrong idea for me.
@user3441734 I can see the logic of this approach.The way the app works is to present the view, and asynchronously get the data types that can be selected from the picker, but in some cases not all of the full list of types are available to be selected. Maybe a better approach is to display a placeholder for the picker until the options are available then only populate segments in the picker that are valid. I’ll give that a go...
|
1

Since iOS 17 you can use the modifier selectionDisabled(_:):

VStack(alignment: .leading, spacing: 2.0) {
    // ...(some views)...
    Picker("", selection: $profileMetricSelection) {
        ForEach(Array(profileMetrics.enumerated()), id: \.offset) { index, metric in
            Text(metric.shortName)
                .selectionDisabled(metricDisabled[index]) // 👈 here
                .tag(index)
        }
    }
    .pickerStyle(.segmented)
    // ...(some more views)...
}

1 Comment

Finally!! After over 5 years and many versions of my program SwiftUI implemented the feature ... many thanks Benzy!!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.