GorCat
GorCat

Reputation: 244

SwiftUI Picker with option for no selection in iOS16

I am using Picker with option for no selection, in iOS15 it is working fine, but in iOS16 it has a default value, how can I remove this default value, I don't need to show the text to the right of the Picker line when selection is nil.

struct ContentView: View {
    
    @State private var selection: String?
    let strengths = ["Mild", "Medium", "Mature"]
    
    var body: some View {
        NavigationView {
            List {
                Section {
                    Picker("Strength", selection: $selection) {
                        ForEach(strengths, id: \.self) {
                            Text($0).tag(Optional($0))
                        }
                    }
                }
            }
        }
    }
}

in iOS15, when selection is nil, no text is displayed on the right side of the Picker row
enter image description here

but in iOS 16, the same code leads to different results, when selection is nil it has a default value enter image description here

Upvotes: 11

Views: 7375

Answers (4)

snowskeleton
snowskeleton

Reputation: 815

My version of this problem involves letting the user go back from selection a value to selecting no value when you want the picker to work directly with your custom types. Other solutions work with generic Int or String by setting the value to 0 or a visibly empty string (e.g., " "). You can't do that with custom types, though.

The secret lies in using nil as your tag, and casting it to your custom type with nil as CustomType? (note that you have to make it optional, with ?).

Here is a fully working example:

import SwiftUI
import SwiftData

@Model
class SampleItem: Identifiable {
    var id = UUID().uuidString
    var name: String
    init(_ name: String) {
        self.name = name
    }
}

struct ExampleView: View {
    var values: [SampleItem] = [
        SampleItem("John"),
        SampleItem("Paul"),
        SampleItem("George"),
        SampleItem("Ringo")
    ]
    @State var selectedValue: SampleItem?
    
    var body: some View {
        Picker("Select a value", selection: $selectedValue) {
            Text("None")
                .tag(nil as SampleItem?)
            ForEach(values, id: \.self) {
                Text($0.name).tag($0)
            }
        }
    }
}

Upvotes: 0

Xiaomu Gu
Xiaomu Gu

Reputation: 31

This is how I solved it:

@State private var showAddNewUserWindow = false
@State private var newUser: String = ""

@State private var selectedUser = "Item 1"
@State private var usersList = ["Item 1", "Item 2", "Item 3", "Item 4"]
@State private var isPickerDisabled = false

Button("—") {
    if let index = usersList.firstIndex(of: selectedUser) {
        usersList.remove(at: index)
        if usersList.count == 0 {
            usersList.insert("No user", at: 0)
            isPickerDisabled = true
        }
        selectedUser = usersList.first!
    }
}
                
Picker("",selection:$selectedUser) {
    ForEach(usersList, id: \.self) {
        Text($0)
    }
                    
}
.disabled(isPickerDisabled)
.onChange(of: selectedUser) {_ in
    usersList.remove(at: usersList.firstIndex(of: selectedUser)!)
    usersList.insert(selectedUser, at: 0)
}
                
Button("+") {
    showAddNewUserWindow = true
}
.popover(isPresented: $showAddNewUserWindow) {
    VStack {
        Spacer()
            .frame(height: 26.0)
                        
        Text("Add new user")
            .font(.title2)
                        
        Spacer()
            .frame(height: 6.0)
                        
        Text("Please enter the name of the new user")
            .font(.caption)
                        
        Spacer()
            .frame(height: 20.0)
                        
        HStack {
            Spacer()
                .frame(width: 24.0)
                            
            TextField("New user's name", text: $newUser)
                .textFieldStyle(.roundedBorder)
                            
            Spacer()
                .frame(width: 24.0)
        }
                        
        Spacer()
            .frame(height: 20.0)
                        
        HStack {
            Button("Cancel") {
                showAddNewUserWindow.toggle()
            }
            .frame(width: 145.0, height: 50.0)
            .border(.quaternary, width: 0.8)
                            
            Button("Add") {
                if newUser.count > 0 {
                    if usersList.contains(newUser) == false {
                        if usersList.first == "No user" {
                            usersList.removeFirst()
                            isPickerDisabled = false
                        }
                                        
                        usersList.insert(newUser, at: 0)
                        selectedUser = newUser
                        newUser = ""
                        showAddNewUserWindow.toggle()
                    } else {
                                        
                    }
                } else {
                                    
                }
            }
            .frame(width: 145.0, height: 50.0)
            .border(.quaternary, width: 0.8)
        }
    }        
}

Upvotes: 0

Mark Burton
Mark Burton

Reputation: 61

This is what I ended up doing for iOS 16.0 from XCode 14.0.1 (to avoid user irritation on iOS 16.0 devices):

let promptText: String = "select" // just a default String
    
//short one for your example
Section {
    Picker("Strength", selection: $selection) {
        if selection == nil { // this will work, since there is no initialization to the optional value in your example
            Text(promptText).tag(Optional<String>(nil)) // is only shown until a selection is made
        }
        ForEach(strengths, id: \.self) {
            Text($0).tag(Optional($0))
        }
    }
}
    
// more universal example
Section {
    Picker("Strength", selection: $selection) {
        if let safeSelection = selection{
            if !strengths.contains(safeSelection){ // does not care about a initialization value as long as it is not part of the collection 'strengths'
                Text(promptText).tag(Optional<String>(nil)) // is only shown until a selection is made
            }
        }else{
            Text(promptText).tag(Optional<String>(nil))
        }
        ForEach(strengths, id: \.self) {
            Text($0).tag(Optional($0))
        }
    }
}
    
// Don't want to see anything if nothing is selected? empty String "" leads to an warning. Go with non visual character like " " or 'Horizontal Tab'. But then you will get an empty row...
Section {
    let charHorizontalTab: String = String(Character(UnicodeScalar(9)))
    Picker("Strength", selection: $selection) {
        if let safeSelection = selection{
            if !strengths.contains(safeSelection){ // does not care about a initialization value as long as it is not part of the collection 'strengths'
                Text(charHorizontalTab).tag(Optional<String>(nil)) // is only shown until a selection is made
            }
        }else{
            Text(charHorizontalTab).tag(Optional<String>(nil))
        }
        ForEach(strengths, id: \.self) {
            Text($0).tag(Optional($0))
        }
    }
}

good luck finding a solution that works for you

Upvotes: 5

Carsten
Carsten

Reputation: 1091

Xcode 14.1 Beta 3 logs: "Picker: the selection "nil" is invalid and does not have an associated tag, this will give undefined results."

To resolve this log you need to add an Option which uses the nil tag.

struct ContentView: View {

    @State private var selection: String?
    let strengths = ["Mild", "Medium", "Mature"]

    var body: some View {
        NavigationView {
            List {
                Section {
                    Picker("Strength", selection: $selection) {
                        Text("No Option").tag(Optional<String>(nil))
                        ForEach(strengths, id: \.self) {
                            Text($0).tag(Optional($0))
                        }
                    }
                    Text("current selection: \(selection ?? "none")")
                }
            }
        }
    }
}

Upvotes: 24

Related Questions