Unable to store selected Picker value in SwiftUI - swift

I have a simple SwiftUI view with a Picker containing a list of objects from a data array. The Picker lists the objects just fine, but the selected value is not being saved to the binding variable $selectedCar. It returns empty string. This is the view in question:
struct GarageSpace: View {
var currentUserID: String
#Environment(\.presentationMode) var presentationMode
#Binding var selectedPlaceID: String
#Binding var selectedPlaceName: String
#Binding var selectedPlaceDate: Date
#Binding var selectedSpaceID: String
#State var selectedCar: String
#Binding var cars: CarArrayObject
var body: some View {
VStack{
Group{
Picker("Car", selection: $selectedCar) {
if let cars = cars{
ForEach(cars.dataArray, id: \.self) {car in
let year = car.year! as String
let make = car.make as String
let model = car.model! as String
let string = year + " " + make + " " + model
Text(string) //displays correctly in Picker
}
}
}
Spacer()
if let cars = cars {
Button {
print("yes")
print(selectedCar) //returns empty string
} label: {
Text("Confirm")
}
}
}
}
}
}
The above view is displayed via a NavigationLink on the previous screen:
NavigationLink(destination: GarageSpace(currentUserID: currentUserID, selectedPlaceID: $selectedPlaceID, selectedPlaceName: $selectedPlaceName, selectedPlaceDate: $selectedPlaceDate, selectedSpaceID: $selectedSpaceID, selectedCar: "", cars: $cars)) {
}
This NavigationLink might be the culprit because I'm sending an empty string for selectedCar. However, it forces me to initialize a value with the NavigationLink.
Any ideas? Thanks!
EDIT:
Added a tag of type String, still same outcome:
Text(string).tag(car.carID)
EDIT: FOUND THE ISSUE! However, I'm still stumped. The selection variable is empty because I wasn't pressing on the Picker since I only had one item in the array. How can I get the Picker to "select" an item if it's the only one in the array by default?

With tag, all works well in my simple tests. Here is my test code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
GarageSpace()
}
}
struct GarageSpace: View {
#State var selectedCar: String = ""
#State var cars: CarArrayObject? = CarArrayObject(car: CarModel(make: "Ford"))
var body: some View {
VStack {
Group {
Picker("Car", selection: $selectedCar) {
if let cars = cars {
ForEach(cars.dataArray, id: \.self) { car in
Text(car.make).tag(car.carID)
}
}
}
Spacer()
if let cars = cars {
Button {
print("----> selectedCar carID: \(selectedCar)")
} label: {
Text("Show selected carID")
}
}
}
}
// optional, to select the first car
.onAppear {
if let cars = cars {
selectedCar = (cars.dataArray.first != nil) ? cars.dataArray.first!.carID : ""
}
}
}
}
struct CarModel: Hashable {
var make = ""
var carID = UUID().uuidString
}
class CarArrayObject: ObservableObject{
// for testing
#Published var dataArray = [CarModel(make: "Toyota"), CarModel(make: "Suzuki"), CarModel(make: "VW")]
/// USED FOR SINGLE CAR SELECTION
init(car: CarModel) {
self.dataArray.append(car)
}
/// USED FOR GETTING CARS FOR USER PROFILE
init(userID: String) {
// print("GET CARS FOR USER ID \(userID)")
// DataService.instance.downloadCarForUser(userID: userID) { (returnedCars) in
//
// let sortedCars = returnedCars.sorted { (car1, car2) -> Bool in
// return car1.dateCreated > car2.dateCreated
// }
// self.dataArray.append(contentsOf: sortedCars)
// }
}
}

Related

Sheet inside ForEach leads to compiler being unable to type-check this expression Error

I am trying to open a sheet when tapping on an item. I followed this questions Sheet inside ForEach doesn't loop over items SwiftUI answer. I get this Error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions, I don't understand what is causing it. I tried multiple solutions and they all lead to the same Error.
#State var selectedSong: Int? = nil
#State var songList: [AlbumSong] = []
VStack {
ForEach(songList.enumerated().reversed(), id: \.offset) { index, song in
HStack {
Text("\(index + 1).").padding(.leading, 8)
VStack {
Text(song.title)
Text(song.artist)
}
}.onTapGesture {
self.selectedSong = index
}
}
}
}
.sheet(item: self.$selectedSong) { selectedMovie in
SongPickerEdit(songList: $songList, songIndex: selectedMovie)
I also tried setting songIndex to being an AlbumSong and then implemented this sheet:
.sheet(item: self.$selectedSong) {
SongPickerEdit(songList: $songList, songIndex: self.songList[$0])
}
struct SongPickerEdit: View {
#Binding var songList: [AlbumSong]
#State var songIndex: Int?
var body: some View {
}
}
struct AlbumSong: Identifiable, Codable {
#DocumentID var id: String?
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
How about making selectedSong an AlbumSong?? The item: parameter needs to be an Identifiable binding, but Int is not Identifiable.
#State var selectedSong: AlbumSong? = nil
#State var songList: [AlbumSong] = []
var body: some View {
List {
ForEach(songList.enumerated().reversed(), id: \.offset) { index, song in
HStack {
Text("\(index + 1).").padding(.leading, 8)
VStack {
Text(song.title)
Text(song.artist)
}
}.onTapGesture {
self.selectedSong = song
}
}
}.sheet(item: $selectedSong) { song in
SongPickerEdit(songList: $songList, song: song)
}
}
Note that SongPickerEdit would look like this:
struct SongPickerEdit: View {
#State var song: AlbumSong
var body: some View {
Text("\(song.title), \(song.artist)")
}
}
If you really need the index for some reason, you can add the song list binding back in and use songList.index { $0.id == song.id } to find the index if the list is not too long.
Otherwise, you can make your own Identifiable type SongAndIndex that uses the same id as AlbumSong, but with an extra index property, and use that as the type of selectedSong.
A third way would be to use the sheet(isPresented:) overload, but this way you end up with 2 sources of truth:
#State var selectedSongIndex: Int? = nil {
didSet {
if selectedSongIndex != nil {
isSheetPresented = true
}
}
}
#State var isSheetPresented: Bool = false {
didSet {
if !isSheetPresented {
selectedSongIndex = nil
}
}
}
...
}.sheet(isPresented: $isSheetPresented) {
SongPickerEdit(songList: $songList, songIndex: selectedSongIndex)
}
selectedSongIndex also won't be set to nil when the user dismisses the sheet.

View dismisses unintentionally in SwiftUI

My goal is to have an array of structs that when tapped on an individual item, a change is made and passed up to the parent view but doesn't automatically dismiss my child view. I'm really not sure why the child view is automatically dismissing and how to prevent it. Here is my code for the ContentView. Note that I have now updated the struct to Identifiable.
//
// ContentView.swift
// test
//
// Created by Kevin McQuown on 3/7/22.
//
import SwiftUI
struct Person: Identifiable {
var id = UUID()
var name: String = ""
}
struct ContentView: View {
#State private var patients: [Person]
init() {
var temp: [Person] = []
for index in 0 ..< 3 {
temp.append(Person(name: "\(index)"))
}
patients = temp
}
var body: some View {
NavigationView {
VStack(spacing: 40) {
ForEach($patients) { $patient in
NavigationLink("\(patient.name)") {
View2(patient: $patient)
}
}
}
}
}
}
struct View2: View {
#Binding var patient: Person
var body: some View {
NavigationLink("\(patient.name)") {
ChangeNameView(patient: $patient)
}
}
}
struct ChangeNameView: View {
#Binding var patient: Person
var body: some View {
Button {
patient.name = "New Name"
} label: {
Text("Set New Name")
}
}
}

Making custom get {} set{} to work like dynamic proxy/shortcut to different objects in Array. (SwiftUI) [duplicate]

This question already has answers here:
How to change a value of struct that is in array?
(2 answers)
Closed 1 year ago.
I'm trying to achieve a two way binding-like functionality.
I have a model with an array of identifiable Items, var selectedID holding a UUID of selected Item, and var proxy which has get{} that looks for an Item inside array by UUID and returns it.
While get{} works well, I can't figure out how to make proxy mutable to change values of selected Item by referring to proxy.
I have tried to implement set{} but nothing works.
import SwiftUI
var words = ["Aaaa", "Bbbb", "Cccc"]
struct Item: Identifiable {
var id = UUID()
var word: String
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(word: "One"), Item(word: "Two"), Item(word: "Three")]
#Published var selectedID: UUID?
var proxy: Item? {
set {
// how to set one property of Item?, but not the whole Item here?
}
get {
let index = items.firstIndex(where: { $0.id == selectedID })
return index != nil ? items[index!] : nil
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
// monitoring
MonitorkVue(model: model)
//selections
HStack {
ForEach(model.items.indices, id:\.hashValue) { i in
SelectionVue(item: $model.items[i], model: model)
}
}
}.padding()
}
}
struct MonitorkVue: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text(model.proxy?.word ?? "no proxy")
// 3rd: cant make item change by referring to proxy
// in order this to work, proxy's set{} need to be implemented somehow..
Button {
model.proxy?.word = words.randomElement()!
} label: {Text("change Proxy")}
}
}
}
struct SelectionVue: View {
#Binding var item: Item
#ObservedObject var model: Model
var body: some View {
VStack {
Text(item.word).padding()
// 1st: making selection
Button {
model.selectedID = item.id } label: {Text("SET")
}.disabled(item.id != model.selectedID ? false : true)
// 2nd: changing item affects proxy,
// this part works ok
Button {
item.word = words.randomElement()!
}label: {Text("change Item")}
}
}
}
Once you SET selection you can randomize Item and proxy will return new values.
But how to make it works the other way around when changing module.proxy.word = "Hello" would affect selected Item?
Does anyone knows how to make this two-way shortct?
Thank You
Here is a correction and some fix:
struct Item: Identifiable {
var id = UUID()
var word: String
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(word: "One"), Item(word: "Two"), Item(word: "Three")]
#Published var selectedID: UUID?
var proxy: Item? {
get {
if let unwrappedIndex: Int = items.firstIndex(where: { value in (selectedID == value.id) }) { return items[unwrappedIndex] }
else { return nil }
}
set(newValue) {
if let unwrappedItem: Item = newValue {
if let unwrappedIndex: Int = items.firstIndex(where: { value in (unwrappedItem.id == value.id) }) {
items[unwrappedIndex] = unwrappedItem
}
}
}
}
}

Create a dynamic list of editable objects in swiftUI, store in the mobile itself

I tried to create a list of editable objects in SwiftUI. Here is my idea.
First of all, the editable item is as follows:
struct Item: Identifiable {
var id: UUID
var ItemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [
.init(id: .init(), ItemNum: 55),
.init(id: .init(), ItemNum: 57),
.init(id: .init(), ItemNum: 87)
]
}
After that I created a list that get data from the ItemStore:
struct ItemView: View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore
var body: some View {
NavigationView {
List {
ForEach(store.items.indexed(), id:\.1.id) {index, item in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
//.onAppear(perform: store.fetch) // want to fetch the data from the store whenever the list appear, however, no idea to perform the function?!
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
The editView:
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text(Text("Item Num: \(item.itemNum)"))
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
My question here:
I would like to fetch the data from 'store' onAppear. but it fails.
After I quit the app, all the previous data gone. How can I make them to keep inside my app, even the app is kill?
Your second question first: In terms of storing (persisting your data), you have many options. The easiest would be to store it in UserDefaults, which I'll show in my example. You could also choose to use CoreData, which would be more of a process to set up, but would give you a more robust solution later on. Many more options like Realm, Firebase, SQLite, etc. exist as well.
struct Item: Identifiable, Codable {
var id: UUID = UUID()
var itemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "savedItems")
}
}
}
let defaultValues : [Item] = [
.init(itemNum: 55),
.init(itemNum: 57),
.init(itemNum: 87)
]
func fetch() {
let decoder = JSONDecoder()
if let savedItems = UserDefaults.standard.object(forKey: "savedItems") as? Data,
let loadedItems = try? decoder.decode([Item].self, from: savedItems) {
items = loadedItems
} else {
items = defaultValues
}
}
}
struct ContentView : View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore = ItemStore()
var body: some View {
NavigationView {
List {
ForEach(Array(store.items.enumerated()), id:\.1.id) { (index,item) in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
.onAppear(perform: store.fetch)
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text("Item Num: \(item.itemNum)")
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
Regarding your first question, the reason that fetch failed is you had no fetch method. Plus, there was nothing to fetch, since the array of items just got populated upon creation of the ItemStore each time.
Notes:
Item now conforms to Codable -- this is what allows it to get transformed into a value that can be saved/loaded from UserDefaults
fetch is now called on onAppear.
Every time the data is changed, didSet is called, saving the new data to UserDefaults
There were a number of typos and things that just plain wouldn't compile in the original code, so make sure that the changes are reflected. Some of those include: enumerated instead of indexed in the ForEach, not calling Text(Text( with nested values, using the same capitalization of itemNum throughout, etc
Important: when testing this, make sure to give the simulator a few seconds after a change to save the data into UserDefaults before killing the app and opening it again.

Storing a selected value from picker to use in other views - SwiftUI

I was very kindly helped to allow my picker to select a value from my Firestore database. What I would like to do is once that value is selected in my picker I would like to be able to show that value in other views. I have tried setting this up using UserDefaults but I'm not sure that's the way to go? If you could suggest a better method I'd be more than grateful. My code currently is below.
The value in the below code returns Unknown School each time but without the user defaults works flawlessly in fetching the data.
Thanks in advance.
import SwiftUI
import Firebase
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData()
#State private var selectedSchool = UserDefaults.standard.string(forKey: "") // `schoolName.id` is of type String
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(schoolData.datas, id: \.id) {
Text($0.name)
}
}
Text("Selected School: \(selectedSchool ?? "Unknown School")")
}
}.navigationBarTitle("School Selection")
}
}
struct SchoolPicker_Previews: PreviewProvider {
static var previews: some View {
SchoolDetailsView()
}
}
class getSchoolData : ObservableObject{
#Published var datas = [schoolName]()
init() {
let db = Firestore.firestore()
db.collection("School Name").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("Name") as! String
self.datas.append(schoolName(id: id, name: name))
}
}
}
}
struct schoolName : Identifiable {
var id : String
var name : String
}
}
First, the UserDefaults key for your variable can't be empty:
#State private var selectedSchool: String = UserDefaults.standard.string(forKey: "selectedSchool") ?? "Unknown School"
Then you can use onReceive to update the variable:
.onReceive(Just(selectedSchool)) {
UserDefaults.standard.set($0, forKey: "selectedSchool")
}
Full code:
import Combine
import Firebase
import SwiftUI
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData()
#State private var selectedSchool = UserDefaults.standard.string(forKey: "selectedSchool")
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(schoolData.datas, id: \.id) {
Text($0.name)
}
}
.onReceive(Just(selectedSchool)) {
UserDefaults.standard.set($0, forKey: "selectedSchool")
}
Text("Selected School: \(selectedSchool)")
}
}.navigationBarTitle("School Selection")
}
}
}
Note that in SwiftUI 2 / iOS 14 you can use #AppStorage instead.