SwiftUI - Picker - enums

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

Related

Select the first item on a list and highlight it when item changes

I have a simple notes app that uses a list on the left and a texteditor on the right. The list has the titles of the notes, and the texteditor its text. When the user changes the text on the note, the list gets sorted to display the most current note (by date) on top, as its first item.
Started with Ventura, if I'm working on a note other than the first one, then that note (the item in the list) jumps to the top when the text is changed, however, if the first item in the list not visible (I'm working on a note that is way down), then when I change the text the item, it jumps to the top, but you don't jump with it. You are now in this state where you have to scroll up to get to the top and reselect the first item.
I tried using DispatchQueue.main.async to force to reselect the item onchange, but regardless of what I try, it doesn't scroll to the top, even when the selected note id is the correct one.
I ran out of ideas or things to try. How can I go back to the first item once the text is changed?
Here's the code:
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
final class DataModel: ObservableObject {
#AppStorage("notes") public var notes: [NoteItem] = []
}
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
#State var selectedNoteId: UUID?
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
.onAppear {
DispatchQueue.main.async {
selectedNoteId = data.notes.first?.id
}
}
.onChange(of: data.notes) { notes in
if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) {
selectedNoteId = data.notes.first?.id
}
}
}
}
struct NoteView: View {
#EnvironmentObject private var data: DataModel
var note: NoteItem
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
data.sortList()
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

SwiftUI 2.0 App crash when Picker selectedValue changed

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

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

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