swiftuipicker

Change picker backgroundcolor based on selection


I have two pickers, one is for priority and the other one is for the selection of a date. I want to change the background of both pickers based on the selection of the priority picker, as example if the priority is high the picker should be red, middle orange and so on... Currently I can only manually set the background color of the picker with an initializer. This is my code for the View:

struct AddWunschSheetView: View {
    
    @State private var titelTextField: String = ""
    @State private var kostenTextField: String = ""
    
    @State private var sliderValue = 1.0
    @State private var amount = 0.5
    
    @State private var prioPicker = 1
    @State private var selection = 1
    
    @Environment(\.dismiss) var dismiss
    
    init() {
        UISegmentedControl.appearance().selectedSegmentTintColor = UIColor.red
        
        let attributes: [NSAttributedString.Key:Any] = [
            .foregroundColor: UIColor.white
        ]
        UISegmentedControl.appearance().setTitleTextAttributes(attributes, for: .selected)
    }
    
    var body: some View {
        NavigationStack {
            Form {
                Section("Artikeldetails") {
                    TextField("", text: $titelTextField, prompt: Text("Titel des Artikels"))
                }
                
                Section("Priority") {
                    Picker("", selection: $prioPicker) {
                        Text("Dringend").tag(1)
                        Text("Hoch").tag(2)
                        Text("Mittel").tag(3)
                        Text("Niedrig").tag(4)
                    }
                    .pickerStyle(.segmented)
                }
                
                Picker("", selection: $selection) {
                    Text("Tag").tag(1)
                    Text("Woche").tag(2)
                    Text("Monat").tag(3)
                }
                .pickerStyle(.segmented)
                
                if selection == 1 {
                    Slider(value: $sliderValue, in: 1...31, step: 1.0, minimumValueLabel: Text("1"), maximumValueLabel: Text("31"), label: {})
                        .padding(.horizontal)
                } else if selection == 2 {
                    Slider(value: $sliderValue, in: 1...52, step: 1.0, minimumValueLabel: Text("1"), maximumValueLabel: Text("52"), label: {})
                        .padding(.horizontal)
                } else if selection == 3 {
                    Slider(value: $sliderValue, in: 1...12, step: 1.0, minimumValueLabel: Text("1"), maximumValueLabel: Text("12"), label: {})
                        .padding(.horizontal)
                }

                HStack {
                    Spacer()
                    Text( String(format: "%.0f", sliderValue))
                    if selection == 1 {
                        Text("Tage bis zum Ereigniss")
                    } else if selection == 2 {
                        Text("Wochen bis zum Ereigniss")
                    } else if selection == 3 {
                        Text("Monate bis zum Ereigniss")
                    }
                    Spacer()
                }
                
                HStack {
                    VStack {
                        Text("Kosten")
                            .bold()
                        TextField("", text: $kostenTextField, prompt: Text("0€"))
                            .keyboardType(.numberPad)
                            .padding(.top)
                    }
                    Spacer()
                    
                    if prioPicker == 1 {
                        Gauge(value: amount) {
                            Text("50%")
                        }
                        .gaugeStyle(.accessoryCircularCapacity)
                        .tint(.red)
                    } else if prioPicker == 2 {
                        Gauge(value: amount) {
                            Text("50%")
                        }
                        .gaugeStyle(.accessoryCircularCapacity)
                        .tint(.orange)
                    } else if prioPicker == 3 {
                        Gauge(value: amount) {
                            Text("50%")
                        }
                        .gaugeStyle(.accessoryCircularCapacity)
                        .tint(.blue)
                    } else if prioPicker == 4 {
                        Gauge(value: amount) {
                            Text("50%")
                        }
                        .gaugeStyle(.accessoryCircularCapacity)
                        .tint(.green)
                    }
                    
                }
                .padding(.horizontal)
            }
            .navigationTitle("Neuen Artikel hinzufügen")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Save") {
                        dismiss()
                    }
                }
            }
        }
    }
}

The UI looks like this:

enter image description here

I have already tried to customize the init with if-statements, I've tried to customize the sections, pickers,... I am not even sure if this is possible.


Solution

  • It is difficult to add styling to a segmented Picker, as you have discovered. However, what you could do is use the same technique as shown in the answer to Segmented picker in iOS - handle tap on already selected item (it was my answer):

    The .matchedGeometryEffect requires a namespace:

    @Namespace private var ns
    

    Your previous init with legacy UI calls is no longer needed

    //    init() {
    //        UISegmentedControl.appearance().selectedSegmentTintColor = UIColor.red
    //
    //        let attributes: [NSAttributedString.Key:Any] = [
    //            .foregroundColor: UIColor.white
    //        ]
    //        UISegmentedControl.appearance().setTitleTextAttributes(attributes, for: .selected)
    //    }
    

    I would suggest creating an enum for your priority levels, something like:

    enum Priority: CaseIterable {
        case dringend
        case hoch
        case mittel
        case niedrig
    
        var asString: String {
            "\(self)".capitalized
        }
    
        var color: Color {
            switch self {
            case .dringend: .red
            case .hoch: .orange
            case .mittel: .blue
            case .niedrig: .green
            }
        }
    }
    

    This enum should be used for the state variable that holds the priority selection:

    @State private var prioPicker = Priority.dringend
    

    The Picker can then be assembled as follows:

    Section("Priority") {
        Picker("", selection: $prioPicker) {
            ForEach(Priority.allCases, id: \.self) { priority in
                Text(priority.asString)
            }
        }
        .pickerStyle(.segmented)
        .background {
    
            // A row of placeholders
            HStack(spacing: 0) {
                ForEach(Priority.allCases, id: \.self) { priority in
                    Color.clear
                        .matchedGeometryEffect(id: priority, in: ns, isSource: true)
                }
            }
        }
        .overlay {
            ZStack {
                prioPicker.color
                Text(prioPicker.asString)
                    .font(.footnote)
                    .fontWeight(.semibold)
                    .foregroundStyle(.white)
            }
            .matchedGeometryEffect(id: prioPicker, in: ns, isSource: false)
            .clipShape(RoundedRectangle(cornerRadius: 7))
            .animation(.spring(duration: 0.28), value: prioPicker)
        }
    }
    

    The Gauge can also be simplified:

    Gauge(value: amount) {
        Text("50%")
    }
    .gaugeStyle(.accessoryCircularCapacity)
    .tint(prioPicker.color)
    

    The same approach can also be used for the segmented picker used to select the period, but giving it the color from the selected priority:

    enum Period: CaseIterable {
        case tag
        case woche
        case monat
    
        var asString: String {
            "\(self)".capitalized
        }
    
        var maxSliderVal: Int {
            switch self {
            case .tag: 31
            case .woche: 52
            case .monat: 12
            }
        }
    }
    
    @State private var selection = Period.tag
    
    Picker("", selection: $selection) {
        ForEach(Period.allCases, id: \.self) { period in
            Text(period.asString)
        }
    }
    .pickerStyle(.segmented)
    .background {
        HStack(spacing: 0) {
            ForEach(Period.allCases, id: \.self) { period in
                Color.clear
                    .matchedGeometryEffect(id: period, in: ns, isSource: true)
            }
        }
    }
    .overlay {
        ZStack {
            prioPicker.color
            Text(selection.asString)
                .font(.footnote)
                .fontWeight(.semibold)
                .foregroundStyle(.white)
        }
        .matchedGeometryEffect(id: selection, in: ns, isSource: false)
        .clipShape(RoundedRectangle(cornerRadius: 7))
        .animation(.spring(duration: 0.28), value: selection)
    }
    
    Slider(
        value: $sliderValue,
        in: 1...Double(selection.maxSliderVal),
        step: 1.0,
        minimumValueLabel: Text("1"),
        maximumValueLabel: Text("\(selection.maxSliderVal)"),
        label: {}
    )
    .padding(.horizontal)
    .onChange(of: selection.maxSliderVal) { oldVal, newVal in
        sliderValue = min(sliderValue, Double(newVal))
    }
    
    HStack {
        Spacer()
        Text( String(format: "%.0f", sliderValue))
        if selection == .tag {
            Text("Tage bis zum Ereigniss")
        } else if selection == .woche {
            Text("Wochen bis zum Ereigniss")
        } else if selection == .monat {
            Text("Monate bis zum Ereigniss")
        }
        Spacer()
    }
    

    Here's how it all looks:

    Animation

    It is interesting to note, that when the RoundedRectangle clip shape is applied to the ZStack, the shape automatically adopts square corners for the inner edges. However, if you would prefer the shape to have four rounded corners in all positions, you just need to move the .clipShape to the color instead.