SwiftUI segmented picker default state as nil - ios

I'm using a segmented picker in a form to answer simple "Yes" and "No" questions. Can the segmented picker initial state be set to nil so that neither yes or no is highlighted/selected on appear?
#State private var equipmentCheck = 0
private var answers = ["Yes", "No"]
var body: some View {
NavigationView {
Form {
Section(header: Text("Questions")) {
Text("Have the plant and equipment been checked?")
Picker("", selection: $equipmentCheck) {
ForEach(0 ..< answers.count, id: \.self) {
Text("\(self.answers[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
}
}

Here is a possible approach. Tested & worked with Xcode 11.4 / iOS 13.4
#State private var equipmentCheck: Int? // << here !!
private var answers = ["Yes", "No"]
var body: some View {
NavigationView {
Form {
Section(header: Text("Questions")) {
Text("Have the plant and equipment been checked?")
Picker("", selection: $equipmentCheck) {
ForEach(0 ..< answers.count, id: \.self) {
Text("\(self.answers[$0])")
.tag(Optional($0)) // << here !!
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
}
}

It seems that if you set the selected value to an index that is not in your segmented control, then none of the values are highlighted.
For example:
#State private var equipmentCheck = -1
works.
Iโ€™m looking for documentation that confirms that this is guaranteed to work, but have found none.
The documentation for selectedSegmentIndex of UIKitโ€™s UISegmentedControl says to set the value to -1 to remove the selection, so it seems reasonable to do so here as well.

Related

Toggling from Picker to Image view causes an index out of range error in SwiftUI

I have a view that uses a button to toggle between a Picker and an Image that is a result of the Picker selection. When quickly toggling from the image to the Picker and immediately back, I get a crash with the following error:
Swift/ContiguousArrayBuffer.swift:600: Fatal error: Index out of range
Toggling less quickly doesn't cause this, nor does toggling in the other direction (picker to image and back). Here is the offending code:
import SwiftUI
struct ContentView: View {
#State private var showingPicker = false
#State private var currentNum = 0
#State private var numbers: [Int] = [1, 2, 3, 4, 5]
var body: some View {
VStack(spacing: 15) {
Spacer()
if showingPicker {
Picker("Number", selection: $currentNum) {
ForEach(0..<numbers.count, id: \.self) {
Text("\($0)")
}
}
.pickerStyle(.wheel)
} else {
Image(systemName: "\(currentNum).circle")
}
Spacer()
Button("Toggle") {
showingPicker = !showingPicker
}
}
}
}
The code works otherwise. I'm new to SwiftUI so I'm still wrapping my head around how views are created/destroyed. I tried changing the order of the properties thinking maybe the array was being accessed before it was recreated(if that's even something that happens) but that had no effect. I also tried ForEach(numbers.indices) instead of ForEach(0..<numbers.count), but it has the same result.
**Edit
I figured out a stop-gap for now. I added #State private var buttonEnabled = true and modified the button:
Button("Toggle") {
showingPicker = !showingPicker
buttonEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
buttonEnabled = true
}
}
.disabled(buttonEnabled == false)
To debounce it. I still want to figure out the problem and make a real fix.
**Edit
Based on comments I've modified the code to take array indexing out of the equation and to better reflect the actual project I'm working on. The code still works, but a quick toggle will cause the exact same crash and error. It also seems to only happen when the .wheel style picker is used, other picker styles don't have this behavior.
enum Icons: String, CaseIterable, Identifiable {
case ear = "Ear"
case cube = "Cube"
case eye = "Eye"
case forward = "Forward"
case gear = "Gear"
func image() -> Image {
switch self {
case .ear:
return Image(systemName: "ear")
case .cube:
return Image(systemName: "cube")
case .eye:
return Image(systemName: "eye")
case .forward:
return Image(systemName: "forward")
case .gear:
return Image(systemName: "gear")
}
}
var id: Self {
return self
}
}
struct ContentView: View {
#State private var showingPicker = false
#State private var currentIcon = Icons.allCases[0]
var body: some View {
VStack(spacing: 15) {
Spacer()
if showingPicker {
Picker("Icon", selection: $currentIcon) {
ForEach(Icons.allCases) {
$0.image()
}
}
.pickerStyle(.wheel)
} else {
currentIcon.image()
}
Spacer()
Button("Toggle") {
showingPicker.toggle()
}
}
}
}
** Edited once more to remove .self, still no change
ForEach is not a for loop, you can't use array.count and id:\.self you need to use a real id param or use the Identifiable protocol.
However if you just need numbers it also supports this:
ForEach(0..<5) { i in
As long as you don't try to look up an array using i.

Why does this SwiftUI List require an extra objectWillChange.send?

Here is a simple list view of "Topic" struct items. The goal is to present an editor view when a row of the list is tapped. In this code, tapping a row is expected to cause the selected topic to be stored as "tappedTopic" in an #State var and sets a Boolean #State var that causes the EditorV to be presented.
When the code as shown is run and a line is tapped, its topic name prints properly in the Print statement in the Button action, but then the app crashes because self.tappedTopic! finds tappedTopic to be nil in the EditTopicV(...) line.
If the line "tlVM.objectWillChange.send()" is uncommented, the code runs fine. Why is this needed?
And a second puzzle: in the case where the code runs fine, with the objectWillChange.send() uncommented, a print statement in the EditTopicV init() shows that it runs twice. Why?
Any help would be greatly appreciated. I am using Xcode 13.2.1 and my deployment target is set to iOS 15.1.
Topic.swift:
struct Topic: Identifiable {
var name: String = "Default"
var iconName: String = "circle"
var id: String { name }
}
TopicListV.swift:
struct TopicListV: View {
#ObservedObject var tlVM: TopicListVM
#State var tappedTopic: Topic? = nil
#State var doEditTappedTopic = false
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(tlVM.topics) { topic in
Button(action: {
tappedTopic = topic
// why is the following line needed?
tlVM.objectWillChange.send()
doEditTappedTopic = true
print("Tapped topic = \(tappedTopic!.name)")
}) {
Label(topic.name, systemImage: topic.iconName)
.padding(10)
}
}
}
Spacer()
}
.sheet(isPresented: $doEditTappedTopic) {
EditTopicV(tlVM: tlVM, originalTopic: self.tappedTopic!)
}
}
}
EditTopicV.swift (Editor View):
struct EditTopicV: View {
#ObservedObject var tlVM: TopicListVM
#Environment(\.presentationMode) var presentationMode
let originalTopic: Topic
#State private var editTopic: Topic
#State private var ic = "circle"
let iconList = ["circle", "leaf", "photo"]
init(tlVM: TopicListVM, originalTopic: Topic) {
print("DBG: EditTopicV: originalTopic = \(originalTopic)")
self.tlVM = tlVM
self.originalTopic = originalTopic
self._editTopic = .init(initialValue: originalTopic)
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save") {
editTopic.iconName = editTopic.iconName.lowercased()
tlVM.change(topic: originalTopic, to: editTopic)
presentationMode.wrappedValue.dismiss()
}
}
HStack {
Text("Name:")
TextField("name", text: $editTopic.name)
Spacer()
}
Picker("Color Theme", selection: $editTopic.iconName) {
ForEach(iconList, id: \.self) { icon in
Text(icon).tag(icon)
}
}
.pickerStyle(.segmented)
Spacer()
}
.padding()
}
}
TopicListVM.swift (Observable Object View Model):
class TopicListVM: ObservableObject {
#Published var topics = [Topic]()
func append(topic: Topic) {
topics.append(topic)
}
func change(topic: Topic, to newTopic: Topic) {
if let index = topics.firstIndex(where: { $0.name == topic.name }) {
topics[index] = newTopic
}
}
static func ex1() -> TopicListVM {
let tvm = TopicListVM()
tvm.append(topic: Topic(name: "leaves", iconName: "leaf"))
tvm.append(topic: Topic(name: "photos", iconName: "photo"))
tvm.append(topic: Topic(name: "shapes", iconName: "circle"))
return tvm
}
}
Here's what the list looks like:
Using sheet(isPresented:) has the tendency to cause issues like this because SwiftUI calculates the destination view in a sequence that doesn't always seem to make sense. In your case, using objectWillSend on the view model, even though it shouldn't have any effect, seems to delay the calculation of your force-unwrapped variable and avoids the crash.
To solve this, use the sheet(item:) form:
.sheet(item: $tappedTopic) { item in
EditTopicV(tlVM: tlVM, originalTopic: item)
}
Then, your item gets passed in the closure safely and there's no reason for a force unwrap.
You can also capture tappedTopic for a similar result, but you still have to force unwrap it, which is generally something we want to avoid:
.sheet(isPresented: $doEditTappedTopic) { [tappedTopic] in
EditTopicV(tlVM: tlVM, originalTopic: tappedTopic!)
}

Get selected item from picker | SwiftUI

I want to get the selected item from a Picker to update some data on the Firebase Database, but when i use the onTapGesture is not working
Note: The items inside the picker are Strings
My Code:
Picker(selection: $numUnitIndex, label: Text("Numerical Unit: \(numUnit)")) {
ForEach(0 ..< units.count) {
Text(self.units[$0]).tag($0).foregroundColor(.blue)
.onTapGesture {
//updateUnit(newUnit: self.units[numUnitIndex])
print("selected \(numUnitIndex)")
}
}
}.pickerStyle(MenuPickerStyle())
Here is a simple example of right way of doing this, no need onTapGesture here:
struct ContentView: View {
let units: [String] = ["๐Ÿ˜€", "๐Ÿ˜Ž", "๐Ÿฅถ", "๐Ÿ˜ก", "๐Ÿ˜"]
#State private var selectedUnit: Int = 0
var body: some View {
Picker(selection: $selectedUnit, label: Text("You selected: \(units[selectedUnit])")) {
ForEach(units.indices, id: \.self) { unitIndex in Text(units[unitIndex]) }
}
.pickerStyle(MenuPickerStyle())
.onChange(of: selectedUnit, perform: { newValue in print("Selected Unit: \(units[newValue])", "Selected Index: \(newValue)")})
}
}
You shouldn't use indices but the objects in the array in your ForEach and there is no need for onTapGesture, the variable passed to selection will hold the selected value.
Something like this
let units: [String] = ["a", "b", "c"]
#State private var selectedUnit: String
init() {
selectedUnit = units.first ?? ""
}
var body: some View {
VStack {
Picker("Units", selection: $selectedUnit) {
ForEach(units, id: \.self) { unit in
Text(unit)
.foregroundColor(.blue)
.font(.largeTitle)
}
}.pickerStyle(MenuPickerStyle())
Text("Selected unit is \(selectedUnit)")
}
}

Segmented picker not letting me pick a different selection

I have a segmented picker with three options that defaults to the first option. The problem is that when I select any of the other ones, it will not change for some reason.
This is what it looks like.
#Binding var transaction: TransactionModel
var body: some View {
VStack {
DatePicker(
"Transaction Date",
selection: $transaction.date,
displayedComponents: [.date]
)
Divider()
Picker("Type", selection: $transaction.type) {
ForEach(TransactionModel.TransactionType.allCases, id:\.self) { tType in
Text(tType.rawValue.capitalized)
}
}.pickerStyle(SegmentedPickerStyle())
}
.padding()
}
This is the code for the view so far, and below is the code for TransactionModel.
struct TransactionModel {
var date = Date()
var type = TransactionType.income
static let `default` = TransactionModel()
enum TransactionType: String, CaseIterable, Identifiable {
case income = "Income"
case expense = "Expense"
case transfer = "Transfer"
var id: String {self.rawValue}
}
}
One thing to note, it will work with the inline picker style. That will let me change between those three options, however, I want to use the segmented one. Can anyone help me figure out what's going on with this?
Edit: I actually found that the inline picker style doesn't seem to be working either. I added some code to display a text view based on what was selected, and it never changed from what it said when Income was selected. But that could be due to my code itself. Below is the code for that.
Picker("Type", selection: $transaction.type) {
ForEach(TransactionModel.TransactionType.allCases, id:\.self) { tType in
Text(tType.rawValue.capitalized)
}
}.pickerStyle(InlinePickerStyle())
Divider()
if(transaction.type == TransactionModel.TransactionType.income) {
Text("Income Selected")
Divider()
}else if(transaction.type == TransactionModel.TransactionType.expense) {
Text("Expense Selected")
Divider()
}

Add new Element in Picker in SwiftUI

I can't find how to add some element in a picker view in SwiftUI, in my sample, I want add "Z" value in picker when I click the button.
struct ContentView: View {
#State var values: [String] = ["A", "B", "C"]
#State private var selectedValue = 0
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedValue, label: Text("Value")) {
ForEach(0 ..< values.count) {
Text(self.values[$0])
}
}
}
Button(action: {
self.values.append("Z")
}, label: {
Text("Add")
})
}.navigationBarTitle("Select a value")
}
}
When I click on the button, Z is added to "values" array but Picker is not refreshed.
Thank you :)
You must identify values by id for SwiftUI to make it's changes detectable:
ForEach(0 ..< self.values.count, id: \.self) {
Text(self.values[$0])
}
This way SwiftIU knowns it should rebuild the picker on change.
Tip: You can use elements directly like this:
ForEach(values, id: \.self) {
Text($0)
}
Don't forget to change the selectedValue type and value to match with the dataSource IF you followed the tip above:
#State private var selectedValue = "A"
Change selectedValue from int to String
#State private var selectedValue = "A"
add the parameter id and the tag modifier
Picker(selection: $selectedValue, label: Text("Value")) {
ForEach(values, id: \.self) {
Text($0).tag(String($0))
}
}

Resources