Segmented picker not letting me pick a different selection - ios

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()
}

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.

SwiftUI segmented picker default state as nil

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.

Text not updating correctly based on picker

This is my model
struct ListItemModel: Codable, Identifiable {
let id: String
let name: String
}
This is the view that will be displayed with a Picker. The list will be populated by an outside source but I simplified it for this example.
struct TypeSelectionView: View {
#State private var selected = 0
let testList = [ListItemModel(id: "11", name: "name1"),
ListItemModel(id: "12", name: "name2")]
var body: some View {
VStack {
Picker(selection: $selected, label: Text("Pick a Type")) {
ForEach(0 ..< testList.count) {
Text(testList[$0].name)
}
}.labelsHidden()
Picker(selection: $selected, label: Text("Pick a Type")) {
ForEach(testList) {type in
Text(type.name)
}
}.labelsHidden()
Text("Selected Type: \(testList[selected].name)")
Spacer()
}
}
}
struct TypeSelectionView_Previews: PreviewProvider {
static var previews: some View {
TypeSelectionView()
}
}
The first Picker is correctly changing the display of the Text view on the page when the Picker changes but the second Picker does not. Is their a way to make the second Picker do the do the same thing where as you change the Picker the Text view will update accordingly
or is the first Picker the way you should always go when making Pickers in SwiftUI?
The reason your second Picker doesn't work is that the values returned by the Picker correspond to the id of the items. In the case of your second Picker, those are String.
You can apply a .tag() to each item, and then the Picker will return that. For example, if you added an explicit tag it would work:
Text(type.name).tag(testList.firstIndex(where: { $0.id == type.id })!)
Alternatively, if you changed your id values to be Int and the id values corresponded to the position in the array, it would work.
Because of the difficulties of implementing a tag, it is easy to see why many developers choose to just iterate on 0 ..< testList.count.
Ok so this my first ever answer for a stack overflow question, I'm quite a newbie myself but hopefully I can be of some help.
The code when placed in to Xcode shows two pickers whose initial values are name1 but when you change the first picker the second picker and the text displaying the selected type change accordingly, but because both pickers share the same source of truth #State private var selected = 0, changing this will have unintended side effects.
import SwiftUI
struct TypeSelectionView: View {
#State private var selected = 0
#State var testList = [ListItemModel(id: "11", name: "name1"),
ListItemModel(id: "12", name: "name2")]
#State var priorityTypes = ["low", "medium", "high", "critical"]
var body: some View {
VStack {
Picker("Pick a Type", selection: $selected) {
ForEach(0..<testList.count) {
Text(self.testList[$0].name)
}
}.labelsHidden()
Picker("Pick a Type", selection: $selected) {
ForEach(0..<testList.count) {
Text(self.testList[$0].name)
}
}.labelsHidden()
Text("Selected Type: \(testList[selected].name)")
Spacer()
}
}
}
struct TypeSelectionView_Previews: PreviewProvider {
static var previews: some View {
TypeSelectionView()
}
}
struct ListItemModel: Codable, Identifiable {
let id: String
let name: String
}

SwiftUI static List weird reuse behavior

I'm facing a strange behavior using a static List in SwiftUI. I can't determine if it's a SwiftUI bug or something I'm doing wrong. I have a very simple List that looks like this :
var body: some View {
List {
SettingsPickerView<TrigonometryUnit>(title: "Trigonometry Units", selection: $viewModel.trigonometryUnitIndex, items: TrigonometryUnit.allCases)
SettingsPickerView<DecimalSeparator>(title: "Decimal Separator", selection: $viewModel.decimalSeparatorIndex, items: DecimalSeparator.allCases)
SettingsPickerView<GroupingSeparator>(title: "Grouping Separator", selection: $viewModel.groupingSeparatorIndex, items: GroupingSeparator.allCases)
SettingsPickerView<ExponentSymbol>(title: "Exponent Symbol", selection: $viewModel.exponentSymbolIndex, items: ExponentSymbol.allCases)
}
}
Each cell of the List looks like this :
struct SettingsPickerView<T: Segmentable>: View {
let title: String
#Binding var selection: Int
let items: [T]
var body: some View {
Section(header: Text(title)) {
ForEach(items.indices) { index in
self.cell(for: self.items[index], index: index)
}
}
}
private func cell(for item: T, index: Int) -> some View {
print(title, item.title, items.map({ $0.title }))
return Button(action: {
self.selection = index
}, label: {
HStack {
Text(item.title)
Spacer()
if index == self.selection {
Image(systemName: "checkmark")
.font(.headline)
.foregroundColor(.rpnCalculatorOrange)
}
}
})
}
}
And finally, this is what a Segmentable object looks like:
enum GroupingSeparator: Int, CaseIterable {
case defaultSeparator
case space
case comma
}
extension GroupingSeparator: Segmentable {
var id: String {
switch self {
case .defaultSeparator:
return "groupingSeparator.default"
case .space:
return "groupingSeparator.space"
case .comma:
return "groupingSeparator.comma"
}
}
var title: String {
switch self {
case .defaultSeparator:
return "Default"
case .space:
return "Space"
case .comma:
return "Comma"
}
}
}
When the SettingsView is loaded. everything looks fine. But as soon as I start scrolling, and some other cells are instantiated, there are some cell displayed, but not the proper ones. Here is some screenshots and logs.
When the view is loaded, no scrolling, here is what the screen looks like:
But, what I got on the console is pretty weird and doesn't follow the order of the SettingsPickerView written in the main View:
Trigonometry Units Radians ["Radians", "Degrees"] <-- Fine
Trigonometry Units Degrees ["Radians", "Degrees"] <-- Fine
Decimal Separator Default ["Default", "Dot", "Comma"] <-- Fine
Decimal Separator Default ["Default", "Dot", "Comma"] <-- Fine
Trigonometry Units Degrees ["Radians", "Degrees"] <-- Not expected. Should be Grouping Separator
Trigonometry Units Radians ["Radians", "Degrees"] <-- Not expected. Should be Grouping Separator
The second section is ok and properly displayed:
But the third section is completely broken:
The third section displays its title properly, but display some of the data of the first section. I tried to add an identifier to the button in the cell because the issue looks like SwiftUI can't identify the proper data. But adding an identifier to the button broke the binding, and the checkbox don't change anymore.
private func cell(for item: T, index: Int) -> some View {
print(title, item.title, items.map({ $0.title }))
return Button(action: {
self.selection = index
}, label: {
HStack {
Text(item.title)
Spacer()
if index == self.selection {
Image(systemName: "checkmark")
.font(.headline)
.foregroundColor(.rpnCalculatorOrange)
}
}
})
.id(UUID().uuidString) // This solve the display issue but broke the binding.
}
Does someone experienced something like this before ?
Thanks in advance for your help.
Here is fixed block of code (due to used indexes only List is confused and reuses rows, so solution is to make rows identifiable by items).
Tested with Xcode 11.4
struct PickerView<T: Segmentable>: View {
// ... other code here
var body: some View {
Section(header: Text(title)) {
// Corrected section construction !!
ForEach(Array(items.enumerated()), id: \.element.id) { index, _ in
self.cell(for: self.items[index], index: index)
}
}
}
// ... other code here

Passing data from enum to sheet triggered inside ForEach loop

I've got an ForEach loop inside my VStack, so that for every element in my enum a new "cell" is created. This works just fine. I can pass the title and the number for each cell, but in each cell there is a button which is toggling a sheet view. Each sheet should contain the according text in a scroll view. The text therefore is also given in the enum.
Problem: But when I'm trying to pass that infoText via the element.infoText for every sheet the infoText of the first element in the enum gets presented.
The ForEach loop:
struct ListView: View{
#State var infoSheetIsPresented: Bool = false
var body: some View{
VStack {
ForEach(WelcomeCardViewContent.allCases, id: \.self) {
element in
HStack {
Text(element.text)
Button(action: {
self.infoSheetIsPresented.toggle()
}) {
Image(systemName: "info.circle")
}
.sheet(isPresented: self.$infoSheetIsPresented) {
Text(element.infoText)
}
}
}
}
}
}
And here is my enum. Of course there's the InfoSheetView as well, but like i said its basically just a scroll view with text. The text gets passed with a simple "text" constant. For simplicity I've replaced the separate sheet view with a simple text view -> same problem.
enum WelcomeCardViewContent: String, CaseIterable{
case personalData
case bodyParameters
var text: String {
switch self{
case .personalData:
return "Personal Data"
case .bodyParameters:
return "Body Parameters"
}
}
var infoText: String {
switch self{
case .personalData:
return "1 Lorem ipsum dolor.."
case .bodyParameters:
return "2 Lorem ipsum dolor sit amet."
}
}
}
Thanks for your advice ^^.
Since you were losing track of the current card, I fixed this by saving the card which is going to be shown. Now the text is displayed correctly.
Here is the fixed version:
struct ListView: View {
#State private var infoSheetIsPresented: Bool = false
#State private var showingCard: WelcomeCardViewContent = .personalData
var body: some View {
ForEach(WelcomeCardViewContent.allCases, id: \.self) { element in
HStack {
Text(element.text)
Button(action: {
self.showingCard = element
self.infoSheetIsPresented.toggle()
}) {
Image(systemName: "info.circle")
}
.sheet(isPresented: self.$infoSheetIsPresented) {
Text(self.showingCard.infoText)
}
}
}
}
}

Resources