SwiftUI 2.0 App crash when Picker selectedValue changed - xcode

Here is my sample code:
import SwiftUI
final class ViewModel: ObservableObject {
#Published var countries: [Country?] = [
Country(id: 0, name: "country1", cities: ["c1 city1", "c1 city2", "c1 city3"]),
Country(id: 1, name: "country2", cities: ["c2 city1", "c2 city2", "c2 city3"]),
Country(id: 2, name: "country3", cities: ["c3 city1", "c3 city2", "c3 city3"])
]
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#State private var selectedCountry: Country? = nil
#State private var selectedCity: String? = nil
var body: some View {
VStack {
Picker("", selection: $selectedCountry) {
ForEach(viewModel.countries, id: \.self) { country in
Text(country!.name).tag(country)
}
}
.pickerStyle(SegmentedPickerStyle())
Text(selectedCountry?.name ?? "no selection")
if selectedCountry != nil {
Picker("", selection: $selectedCity) {
ForEach(selectedCountry!.cities, id: \.self) { city in
Text(city!).tag(city)
}
}
.pickerStyle(SegmentedPickerStyle())
Text(selectedCity ?? "no selection")
}
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Country: Codable, Hashable, Identifiable {
var id: Int
var name: String
var cities: [String?]
}
It works at first but when select a country then select another country then go back to the first choice it crashes,
I am using latest Xcode beta I don't know if it is the cause or my approach is wrong.

The problem is in cached binding. We need to recreate picker if source of data changed.
Find below a fix. Tested with Xcode 12.4 / iOS 14.4
if selectedCountry != nil {
Picker("", selection: $selectedCity) {
ForEach(selectedCountry!.cities, id: \.self) { city in
Text(city!).tag(city)
}
}
.pickerStyle(SegmentedPickerStyle())
.id(selectedCountry!) // << here !!
Text(selectedCity ?? "no selection")
}

Related

The picker doesn't update the number of segments it has when a value is changed during execution

I have recently started learning swiftUI and I'm facing some issues here. This is the code:
struct ContentView: View {
#State private var measurementType = 0
#State private var inputValue = ""
#State private var inputUnit = 0
#State private var outputUnit = 1
var outputValue = ""
let measurementTypes = ["Temp", "Length", "Time", "Volume"]
var typeDictionary = [
["Celsius", "Fahrenheit", "Kelvin"],
["Meters", "Kilometers", "Feet", "Yards", "Miles"],
["Seconds", "Minutes", "Hours", "Days"],
["Milliliters", "Liters", "Cups", "Pints", "Gallons"]
]
var body: some View {
NavigationView {
Form {
Section(header: Text("Choose the type of measurement")) {
Picker("The type of measurement", selection: $measurementType) {
ForEach(0 ..< measurementTypes.count) {
Text("\(measurementTypes[$0])")
}
}
.id(measurementType)
.pickerStyle(SegmentedPickerStyle())
}
Section {
TextField("Enter the value", text: $inputValue)
.keyboardType(.decimalPad)
}
Section(header: Text("Choose the input unit")) {
Picker("The input unit", selection: self.$inputUnit) {
ForEach(0 ..< typeDictionary[measurementType].count) {
Text("\(typeDictionary[measurementType][$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
Section(header: Text("Choose the output unit")) {
}
Section(header: Text("Converted Value")) {
Text("")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Whenever I change the measurementType while the code is running, the values of the picker in the input units section update, but the number of segments don't. If I start with the value of measurementType as 1, then the app simply crashes when I choose Temp or Time from the picke.
When using ForEach, you need to provide an id so SwiftUI can uniquely identify each element in the array and know when to redraw the view. You can use id: \.self to use the item's value as its id.
Here's an updated working version:
import SwiftUI
struct ContentView: View {
#State private var measurementType = 0
#State private var inputValue = ""
#State private var inputUnit = 0
#State private var outputUnit = 1
var outputValue = ""
let measurementTypes = ["Temp", "Length", "Time", "Volume"]
var typeDictionary = [
["Celsius", "Fahrenheit", "Kelvin"],
["Meters", "Kilometers", "Feet", "Yards", "Miles"],
["Seconds", "Minutes", "Hours", "Days"],
["Milliliters", "Liters", "Cups", "Pints", "Gallons"]
]
var body: some View {
NavigationView {
Form {
Section(header: Text("Choose the type of measurement")) {
Picker("The type of measurement", selection: $measurementType) {
ForEach(0 ..< measurementTypes.count, id: \.self) {
Text("\(measurementTypes[$0])")
}
}
.id(measurementType)
.pickerStyle(SegmentedPickerStyle())
}
Section {
TextField("Enter the value", text: $inputValue)
.keyboardType(.decimalPad)
}
Section(header: Text("Choose the input unit")) {
Picker("The input unit", selection: self.$inputUnit) {
ForEach(0 ..< typeDictionary[measurementType].count, id: \.self) {
Text("\(typeDictionary[measurementType][$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
Section(header: Text("Choose the output unit")) {
}
Section(header: Text("Converted Value")) {
Text("")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI - ObservableObject, EnvironmentObject, SummaryView

After creating the code, i would like to create a summary view that allows me to view the values that have been chosen in the picker.
How can I do? I read some forums about #ObservableObject and about #EnvironmentObject, but I can't understand ...
Thanks very much :)
import SwiftUI
//SUMMARYPAGE
struct SummaryView: View {
var body: some View {
NavigationView {
Form {
VStack(alignment: .leading, spacing: 6) {
Text("First Animal: \("firstAnimalSelected")")
Text("First Animal: \("secondAnimalSelected")")
}
}
}
}
}
struct SummaryView_Previews: PreviewProvider {
static var previews: some View {
SummaryView()
}
}
enum Animal: String, CaseIterable {
case select
case bear
case cat
case dog
case lion
case tiger
}
struct ContentView: View {
#State private var firstAnimal = Animal.allCases[0]
#State private var secondAnimal = Animal.allCases[0]
var body: some View {
NavigationView {
Form {
Section(header: Text("Animals")
.foregroundColor(.black)
.font(.system(size: 15))
.fontWeight(.bold)) {
Picker(selection: $firstAnimal, label: Text("Select first animal")) {
ForEach(Animal.allCases, id: \.self) { element in
Text(element.rawValue.capitalized)
}
}
Picker(selection: $secondAnimal, label: Text("Select second animal")) {
ForEach(Animal.allCases.filter { $0 != firstAnimal || firstAnimal == .select }, id: \.self) { element2 in
Text(element2.rawValue.capitalized)
}
}
}.font(.system(size: 15))
}.navigationBarTitle("List", displayMode: .inline)
}
}
}
You can move your #State properties to an ObservableObject:
class ViewModel: ObservableObject {
#Published var firstAnimal = Animal.allCases[0]
#Published var secondAnimal = Animal.allCases[0]
}
and access them from an #EnvironmentObject:
struct ContentView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
...
Picker(selection: $viewModel.firstAnimal, label: Text("Select first animal")) {
ForEach(Animal.allCases, id: \.self) { element in
Text(element.rawValue.capitalized)
}
}
}
}
struct SummaryView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
Form {
VStack(alignment: .leading, spacing: 6) {
Text("First Animal: \(viewModel.firstAnimal.rawValue)")
Text("Second Animal: \(viewModel.secondAnimal.rawValue)")
}
}
}
}
}
Remember to inject your ViewModel to your root view:
ContentView().environmentObject(ViewModel())

SwiftUI - Picker

I am learning swift, now I have inserted 2 pickers that call the struct Animal.
What I can't understand is how to tell swift that if the first picker has selected an enum value, that same value must not be present in the enum available to the second picker, precisely because it has already been chosen.
Thanks so much :)
import SwiftUI
enum Animal: String, CaseIterable {
case selectCase = "Select"
case bear = "Bear"
case cat = "Cat"
case dog = "Dog"
case lion = "Lion"
case tiger = "Tiger"
static var animals: [String] = [selectCase.rawValue, bear.rawValue, cat.rawValue, dog.rawValue, lion.rawValue, tiger.rawValue]
}
struct ContentView: View {
#State private var Picker1: String = Animal.animals[0]
#State private var Picker2: String = Animal.animals[0]
var body: some View {
NavigationView {
Form {
Section(header: Text("Animals")
.foregroundColor(.black)
.font(.system(size: 15))
.fontWeight(.bold)) {
Picker(selection: $Picker1, label: Text("Select first animal")) {
ForEach(Animal.animals, id: \.self) { element in
Text(element)
}
}
Picker(selection: $Picker2, label: Text("Select second animal")) {
ForEach(Animal.animals, id: \.self) { element2 in
Text(element2)
}
}
}.font(.system(size: 15))
}.navigationBarTitle("List", displayMode: .inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Use filter to filter out the selected element from the first Picker unless the selected case is selectCase. Also made a few improvements for you.
Since the enum already conforms to CaseIterable protocol you don't have to create a custom animals array.
Also, you can use capitalized on rawValue instead of providing rawValue for each enum case which feels redundant.
Properties names should start with a lowercase letter and naming them Picker1 cases confusion, naming the selected animal as firstAnimal or selectedFirstAnimal would be more convenient in the future.
Here's the code.
enum Animal: String, CaseIterable {
case select
case bear
case cat
case dog
case lion
case tiger
}
struct ContentView: View {
#State private var firstAnimal = Animal.allCases[0]
#State private var secondAnimal = Animal.allCases[0]
var body: some View {
NavigationView {
Form {
Section(header: Text("Animals")
.foregroundColor(.black)
.font(.system(size: 15))
.fontWeight(.bold)) {
Picker(selection: $firstAnimal, label: Text("Select first animal")) {
ForEach(Animal.allCases, id: \.self) { element in
Text(element.rawValue.capitalized)
}
}
Picker(selection: $secondAnimal, label: Text("Select second animal")) {
ForEach(Animal.allCases.filter { $0 != firstAnimal || firstAnimal == .select }, id: \.self) { element2 in
Text(element2.rawValue.capitalized)
}
}
}.font(.system(size: 15))
}.navigationBarTitle("List", displayMode: .inline)
}
}
}

How to access value from an item in ForEach list

How to access values from particular item on the list made with ForEach?
As you can see I was trying something like this (and many other options):
Text(item[2].onOff ? "On" : "Off")
I wanted to check the value of toggle of 2nd list item (for example) and update text on the screen saying if it's on or off.
And I know that it's something to do with #Binding and I was searching examples of this and trying few things, but I cannot make it to work. Maybe it is a beginner question. I would appreciate if someone could help me.
My ContentView:
struct ContentView: View {
// #Binding var onOff : Bool
#State private var onOff = false
#State private var test = false
var body: some View {
NavigationView {
List {
HStack {
Text("Is 2nd item on or off? ")
Text(onOff ? "On" : "Off")
// Text(item[2].onOff ? "On" : "Off")
}
ForEach((1...15), id: \.self) {item in
ListItemView()
}
}
.navigationBarTitle(Text("List"))
}
}
}
And ListItemView:
import SwiftUI
struct ListItemView: View {
#State private var onOff : Bool = false
// #Binding var onOff : Bool
var body: some View {
HStack {
Text("99")
.font(.title)
Text("List item")
Spacer()
Toggle(isOn: self.$onOff) {
Text("Label")
}
.labelsHidden()
}
}
}
I don't know what exactly you would like to achieve, but I made you a working example:
struct ListItemView: View {
#ObservedObject var model: ListItemModel
var body: some View {
HStack {
Text("99")
.font(.title)
Text("List item")
Spacer()
Toggle(isOn: self.$model.switchedOnOff) {
Text("Label")
}
.labelsHidden()
}
}
}
class ListItemModel: ObservableObject {
#Published var switchedOnOff: Bool = false
}
struct ContentView: View {
#State private var onOff = false
#State private var test = false
#State private var list = [
(id: 0, model: ListItemModel()),
(id: 1, model: ListItemModel()),
(id: 2, model: ListItemModel()),
(id: 3, model: ListItemModel()),
(id: 4, model: ListItemModel())
]
var body: some View {
NavigationView {
List {
HStack {
Text("Is 2nd item on or off? ")
Text(onOff ? "On" : "Off")
// Text(item[2].onOff ? "On" : "Off")
}
ForEach(self.list, id: \.id) {item in
ListItemView(model: item.model)
}
}
.navigationBarTitle(Text("List"))
}.onReceive(self.list[1].model.$switchedOnOff, perform: { switchedOnOff_second_item in
self.onOff = switchedOnOff_second_item
})
}
}
The #Published basically creates a Publisher, which the UI can listen to per onReceive().
Play around with this and you will figure out what these things do!
Good luck :)
import SwiftUI
struct ContentView: View {
#State private var onOffList = Array(repeating: true, count: 15)
var body: some View {
NavigationView {
List {
HStack {
Text("Is 2nd item on or off? ")
Text(onOffList[1] ? "On" : "Off")
}
ForEach((onOffList.indices), id: \.self) {idx in
ListItemView(onOff: self.$onOffList[idx])
}
}
.navigationBarTitle(Text("List"))
}
}
}
struct ListItemView: View {
#Binding var onOff : Bool
var body: some View {
HStack {
Text("99")
.font(.title)
Text("List item")
Spacer()
Toggle(isOn: $onOff) {
Text("Label")
}
.labelsHidden()
}
}
}
I understand that you are directing me to use ObservableObject. And probably it's the best way to go with final product. But I am still thinking about #Binding as I just need to pass values better between 2 views only. Maybe I still don't understand binding, but I came to this solution.
struct ContentView: View {
// #Binding var onOff : Bool
#State private var onOff = false
// #State private var test = false
var body: some View {
NavigationView {
List {
HStack {
Text("Is 2nd item on or off? ")
Text(onOff ? "On" : "Off")
// Text(self.item[2].$onOff ? "On" : "Off")
// Text(item[2].onOff ? "On" : "Off")
}
ForEach((1...15), id: \.self) {item in
ListItemView(onOff: self.$onOff)
}
}
.navigationBarTitle(Text("List"))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and ListItemView:
import SwiftUI
struct ListItemView: View {
// #State private var onOff : Bool = false
#Binding var onOff : Bool
var body: some View {
HStack {
Text("99")
.font(.title)
Text("List item")
Spacer()
Toggle(isOn: self.$onOff) {
Text("Label")
}
.labelsHidden()
}
}
}
What is happening now is text is being updated after I tap toggle. But I have 2 problems:
tapping 1 toggle changes all of them. I think it's because of this line:
ListItemView(onOff: self.$onOff)
I still cannot access value of just one row. In my understanding ForEach((1...15), id: .self) make each row have their own id, but I don't know how to access it later on.

SwiftUI Picker with Enum Source Is Not Enabled

I'm trying to understand the new SwiftUI picker style, especially with data from a source other than an array. I have built a picker with an enum. I first made a simple app with only the picker and associated enum. This works as expected.
Strangely, when I copy and paste that code into another app with other controls in the form, the picker seems to be inactive. I see it, but cannot click it.
Here's the first app (the picker works):
struct ContentView: View {
#State private var selectedVegetable = VegetableList.asparagus
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedVegetable, label: Text("My Vegetables")) {
ForEach(VegetableList.allCases) { v in
Text(v.name).tag(v)
//use of tag makes no difference
}
}
}
}
.navigationBarTitle("Picker with Enum")
}
}
}
enum VegetableList: CaseIterable, Hashable, Identifiable {
case asparagus
case celery
case shallots
case cucumbers
var name: String {
return "\(self)".map {
$0.isUppercase ? " \($0)" : "\($0)" }.joined().capitalized
}
var id: VegetableList {self}
}
Here's the app with other controls (picker does not work).
struct Order {
var includeMustard = false
var includeMayo = false
var quantity: Int = 1
var avocadoStyle = PepperoniStyle.sliced
var vegetableType = VegetableType.none
var breadType = BreadType.wheat
}
struct OrderForm: View {
#State private var order = Order()
#State private var comment = "No Comment"
#State private var selectedVegetable = VegetableType.asparagus
#State private var selectedBread = BreadType.rye
func submitOrder() {}
var body: some View {
Form {
Text("Vegetable Ideas")
.font(.title)
.foregroundColor(.green)
Section {
Picker(selection: $selectedVegetable, label: Text("Vegetables")) {
ForEach(VegetableType.allCases) { v in
Text(v.name).tag(v)
}
}
Picker(selection: $selectedBread, label: Text("Bread")) {
ForEach(BreadType.allCases) { b in
Text(b.name).tag(b)
}
}
}
Toggle(isOn: $order.includeMustard) {
Text("Include Mustard")
}
Toggle(isOn: $order.includeMayo) {
Text("Include Mayonaisse")
}
Stepper(value: $order.quantity, in: 1...10) {
Text("Quantity: \(order.quantity)")
}
TextField("Say What?", text: $comment)
Button(action: submitOrder) {
Text("Order")
}
}
.navigationBarTitle("Picker in Form")
.padding()
}
}
enum PepperoniStyle {
case sliced
case crushed
}
enum BreadType: CaseIterable, Hashable, Identifiable {
case wheat, white, rye, sourdough, seedfeast
var name: String { return "\(self)".capitalized }
var id: BreadType {self}
}
enum VegetableType: CaseIterable, Hashable, Identifiable {
case none
case asparagus
case celery
case shallots
case cucumbers
var name: String {
return "\(self)".map {
$0.isUppercase ? " \($0)" : "\($0)" }.joined().capitalized
}
var id: VegetableType {self}
}
Xcode 11 Beta 7, Catalina Beta 7
There is no behavior difference between Preview and Simulator .I must be missing
something simple here. Any guidance would be appreciated.
I wrapped the Form in a NavigationView and the pickers now operate as expected. I need to research that once the documentation is more complete but perhaps this can help someone else.

Resources