How do I get a Swift UI Picker to update to the selected value when pulling these values from a database? - ios

Problem
My Picker won't update when I select a new value.
When I click/tap another value, the picker closes and displays "Austin", no matter what I choose.
I am using:
Xcode Version 14.2
I have provided three pieces of code below:
Swift UI View that displays the Picker
Data Model
JSON Data File
Below is a screenshot of the picker:
Here is the code with the Picker:
import SwiftUI
struct AddEditExpenseView: View {
#EnvironmentObject var destinationsModelData: DestinationsModelData
var destinationIndex: Int {
destinationsModelData.destinations.firstIndex(where: { $0.id == destination.id })!
}
var destination: Destination
#State var selectedDestination: String = ""
#State var saveDestinationFieldTextArray: [String] = []
var body: some View {
NavigationView {
Form {
Section(header: Text("Destination")) {
Picker("Destination", selection: $selectedDestination) {
ForEach(destinationsModelData.destinations) { destination in
Text(destination.name)
}
}
.onAppear {
selectedDestination = destination.name
}
.foregroundColor(.black)
}
}
.formStyle(GroupedFormStyle())
.accentColor(.black)
.navigationBarTitle(Text("Add, Edit Expense"))
.navigationBarBackButtonHidden(true)
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct AddEditExpenseView_Previews: PreviewProvider {
static let destinationsModelData = DestinationsModelData()
static var previews: some View {
AddEditExpenseView(destination: destinationsModelData.destinations[0])
.environmentObject(destinationsModelData)
}
}
Here is the Destination Model:
import Foundation
import SwiftUI
import CoreLocation
struct Destination: Hashable, Codable, Identifiable {
var id: Int
var name: String
var city: String
var state: String
var country: String
var description: String
var isOpen: Bool
var isCompared: Bool
private var imageName: String
var image: Image {
Image(imageName)
}
private var coordinates: Coordinates
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D (
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
I have a JSON file that stores the data:
[
{
"name": "Austin",
"category": "Cities",
"city": "Austin",
"state": "Texas",
"country": "USA",
"id": 1001,
"isOpen": true,
"isCompared": true,
"coordinates": {
"longitude": -97.743057,
"latitude": 30.267153
},
"description": "placeholder text",
"imageName": "Austin_TX"
},
{destination2},
{destination3}
]
I tried to use a #FetchRequest but I get an error message that #FetchRequest is not supported.
I tried .onChange with no luck. I couldn't get to the compiling stage here.

To make your Picker display the selection, the #State var selectedDestination: String must
match the type of the .tag() of the Picker element. Such as
Picker("Destination", selection: $selectedDestination) {
ForEach(destinationsModelData.destinations) { destination in
Text(destination.name).tag(destination.name) // <-- here
}
}
This is assuming all destination.name are unique.
A better approach would be to use the .tag(destination.id), and #State var selectedDestination: Int

Related

Add rows from button press (nested array)

I am trying to add rows to a view as the user presses the add button. There are two buttons. One which adds a card and one which adds an expense inside the card. Im confident I have the code working to add cards but when I try to add an Expense inside a card it adds expenses to every card that is shown. How can I make it so that when the user presses the add expense button only the expense rows are added to the one card.
I have two structs one for Card and one for Expense, that I am using to store data.
struct Card: Identifiable {
var id = UUID()
var title: String
var expenses: [Expense]
}
struct Expense: Identifiable {
var id = UUID()
var expenseType: String
var amount: Double = 0.0
}
ContentView()
struct ContentView: View {
#State private var cards = [Card]()
#State private var expense = [Expense]()
var title = ""
var expenseType = ""
var amount: Double = 0.0
var body: some View {
NavigationStack {
Form {
List {
Button("Add card") {
addCard()
}
ForEach($cards) { a in
Section {
TextField("Title", text: a.title)
Button("Add expense") {
addExpenses()
}
ForEach($expense) { b in
TextField("my expense", text: b.expensetype)
TextField("amount", value: b.amount, format: .number)
}
}
}
}
}
}
}
func addCard() {
cards.append(Card(title: title, expenses: expense))
}
func addExpenses() {
expense.append(Expense(expenseType: "", amount: 0.0))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any help would be really appreciated.....
It doesn't seem like you need the following line, because each card has an expense array, you should remove it.
#State private var expense = [Expense]()
Then move the addExpenses func inside struct Card
struct Card: Identifiable {
var id = UUID()
var title: String
var expenses: [Expense]
mutating func addExpenses() {
expenses.append(Expense(expenseType: "", amount: 0.0))
}
}
Then call
a.wrappedValue.addExpenses()
In the Button
Button("Add expense") {
a.wrappedValue.addExpenses()
}

How to loop HashMap style in the View in SWIFTUI

var someProtocol = [SurveyItems : [Surveys]]()
sectionLabels.forEach{ a in
var finalSurveys = [Surveys]()
surveys.forEach{ b in
if a.groupHeader == b.group_survey {
finalSurveys.append(b)
}
someProtocol[a] = finalSurveys
}
}
I wanted to use that someProtocol to dynamically display the label section and the surveys under that section.
for (Surveys, SurveyItems) in someProtocol {
Text(Surveys.sectionTitle)
for survey in SurveyItems {
Text(survey.label)
}
}
I tried ViewBuider but getting some error.
To loop and display your someProtocol dictionary in a View, try this example code:
Adjust the code for your own purpose. Note that in a SwiftUI View you need to use a ForEach not the "normal" swift for x in ... to loop over a sequence.
struct ContentView: View {
#State var someProtocol = [SurveyItems : [Surveys]]()
var body: some View {
List(Array(someProtocol.keys), id: \.self) { key in
VStack {
if let surveys = someProtocol[key] {
Text(key.title).foregroundColor(.red)
ForEach(surveys, id: \.self) { survey in
Text("survey \(survey.label)")
}
}
}
}
.onAppear {
// for testing
someProtocol[SurveyItems(id: "1", number: 1, title: "title-1")] = [Surveys(id: "s1", label: "label-1"), Surveys(id: "s2", label: "label-2")]
someProtocol[SurveyItems(id: "2", number: 2, title: "title-2")] = [Surveys(id: "s3", label: "label-3")]
}
}
}
struct SurveyItems: Identifiable, Hashable {
let id: String
let number: Int
var title: String
}
struct Surveys: Identifiable, Hashable {
let id: String
let label: String
}

Binding with optional struct

I get the error "Value of optional type 'Photo?' must be unwrapped to refer to member 'name' of wrapped base type 'Photo'" when I try to send a optional struct on a binding of a TextField.
The content view code example:
import SwiftUI
struct ContentView: View {
#StateObject var viewModel = ContentViewViewModel()
private var photos = [
Photo(id: UUID(), name: "Exclamation", data: (UIImage(systemName: "exclamationmark.triangle")?.jpegData(compressionQuality: 1))!),
Photo(id: UUID(), name: "Circle", data: (UIImage(systemName: "circle.fill")?.jpegData(compressionQuality: 1))!)
]
var body: some View {
VStack {
DetailView(viewModel: viewModel)
}
.onAppear {
viewModel.selectedPhoto = photos[0]
}
}
}
The view model code example:
import Foundation
#MainActor
final class ContentViewViewModel: ObservableObject {
#Published var photos = [Photo]()
#Published var selectedPhoto: Photo?
}
The detail view code example (that uses the content view's view model):
import SwiftUI
struct DetailView: View {
#ObservedObject var viewModel: ContentViewViewModel
var body: some View {
TextField("Photo name here...", text: $viewModel.selectedPhoto.name)
}
}
Note that for some reasons I need the selectedPhoto property be optional.
You can create your own custom #Binding, so you can handle the process between getting data and set the updated here is an example:
If what you want is to select the photo in a List or Foreach you can bind directly to the array.
import SwiftUI
struct Photo {
let id: UUID
var name: String
let data: Data
}
#MainActor
final class ContentViewViewModel: ObservableObject {
#Published var photos = [Photo]()
#Published var selectedPhoto: Photo?
}
struct optionalBinding: View {
#StateObject var viewModel = ContentViewViewModel()
private var photos = [
Photo(id: UUID(), name: "Exclamation", data: (UIImage(systemName: "exclamationmark.triangle")?.jpegData(compressionQuality: 1))!),
Photo(id: UUID(), name: "Circle", data: (UIImage(systemName: "circle.fill")?.jpegData(compressionQuality: 1))!)
]
var body: some View {
VStack {
DetailView2(viewModel: viewModel)
Button("Select Photo") {
viewModel.selectedPhoto = photos[0]
}
}
}
}
struct DetailView2: View {
#ObservedObject var viewModel: ContentViewViewModel
var body: some View {
Text(viewModel.selectedPhoto?.name ?? "No photo selected")
TextField("Photo name here...", text: optionalBinding())
}
func optionalBinding() -> Binding<String> {
return Binding<String>(
get: {
guard let photo = viewModel.selectedPhoto else {
return ""
}
return photo.name
},
set: {
guard let _ = viewModel.selectedPhoto else {
return
}
viewModel.selectedPhoto?.name = $0
//Todo: also update the array
}
)
}
}

How to update attributes of a struct with TextFields made in ForEach

In SwiftUI I have a list of menu items that each hold a name, price etc. There are a bunch of categories and under each are a list of items.
struct ItemList: Identifiable, Codable {
var id: Int
var name: String
var picture: String
var list: [Item]
#State var newItemName: String
}
I was looking for a way to create a TextField inside each category that would add to its array of items.
Making the TextFields through a ForEach loop was simple enough, but I got stuck trying to add a new Item using the entered text to the right category.
ForEach(menu.indices) { i in
Section(header: Text(menu[i].name)) {
ForEach(menu[i].list) { item in
Text(item.name)
}
TextField("New Type:", text: /*some kind of bindable here?*/) {
menu[i].list.append(Item(name: /*the text entered above*/))
}
}
}
I considered using #Published and Observable Object like this other question, but I need the ItemList to be a Codable struct so I couldn't figure out how to fit the answers there to my case.
TextField("New Type:", text: menu[i].$newItemName)
Anyway any ideas would be appreciated, thanks!
You just have to focus your View.
import SwiftUI
struct ExpandingMenuView: View {
#State var menu: [ItemList] = [
ItemList(name: "Milk Tea", picture: "", list: [ItemModel(name: "Classic Milk Tea"), ItemModel(name: "Taro milk tea")]),
ItemList(name: "Tea", picture: "", list: [ItemModel(name: "Black Tea"), ItemModel(name: "Green tea")]),
ItemList(name: "Coffee", picture: "", list: [])
]
var body: some View {
List{
//This particular setup is for iOS15+
ForEach($menu) { $itemList in
ItemListView(itemList: $itemList)
}
}
}
}
struct ItemListView: View {
#Binding var itemList: ItemList
#State var newItemName: String = ""
var body: some View {
Section(header: Text(itemList.name)) {
ForEach(itemList.list) { item in
Text(item.name)
}
TextField("New Type:", text: $newItemName, onCommit: {
//When the user commits add to array and clear the new item variable
itemList.list.append(ItemModel(name: newItemName))
newItemName = ""
})
}
}
}
struct ItemList: Identifiable, Codable {
var id: UUID = UUID()
var name: String
var picture: String
var list: [ItemModel]
//#State is ONLY for SwiftUI Views
//#State var newItemName: String
}
struct ItemModel: Identifiable, Codable {
var id: UUID = UUID()
var name: String
}
struct ExpandingMenuView_Previews: PreviewProvider {
static var previews: some View {
ExpandingMenuView()
}
}
If you aren't using Xcode 13 and iOS 15+ there are many solutions in SO for Binding with array elements. Below is just one of them
ForEach(menu) { itemList in
let proxy = Binding(get: {itemList}, set: { new in
let idx = menu.firstIndex(where: {
$0.id == itemList.id
})!
menu[idx] = new
})
ItemListView(itemList: proxy)
}
Also note that using indices is considered unsafe. You can watch Demystifying SwiftUI from WWDC2021 for more details.
You can have an ObservableObject to be your data model, storing categories which then store the items.
You can then bind to these items, using Swift 5.5 syntax. This means we can write List($menu.categories) { $category in /* ... */ }. Then, when we write $category.newItem, we have a Binding<String> to the newItem property in Category.
Example:
struct ContentView: View {
#StateObject private var menu = Menu(categories: [
Category(name: "Milk Tea", items: [
Item(name: "Classic Milk Tea"),
Item(name: "Taro Milk Tea")
]),
Category(name: "Tea", items: [
Item(name: "Black Tea"),
Item(name: "Green Tea")
]),
Category(name: "Coffee", items: [
Item(name: "Black Coffee")
])
])
var body: some View {
List($menu.categories) { $category in
Section(header: Text(category.name)) {
ForEach(category.items) { item in
Text(item.name)
}
TextField("New item", text: $category.newItem, onCommit: {
guard !category.newItem.isEmpty else { return }
category.items.append(Item(name: category.newItem))
category.newItem = ""
})
}
}
}
}
class Menu: ObservableObject {
#Published var categories: [Category]
init(categories: [Category]) {
self.categories = categories
}
}
struct Category: Identifiable {
let id = UUID()
let name: String
var items: [Item]
var newItem = ""
}
struct Item: Identifiable {
let id = UUID()
let name: String
}
Result:

How to create new instance of object and pass it into array SwiftUI

I want to create simple program for edit this JSON : https://pastebin.com/7jXyvi6Y
I created Smoothie struct and read smoothies into array.
Now I want create new Smoothie instance which I should pass as parameter into SmoothieForm. In Smoothie form I should complete fields with values and then this smoothie should be added to array and array should be saved in json.
How to create new instance of this Smoothie struct ? And how append into array ?
I have struct with my smoothies
import Foundation
import SwiftUI
struct Smoothie : Hashable, Codable, Identifiable {
var id: Int
var name: String
var category: Category
var wasDone: Bool
var isFavorite: Bool
var time: String
var ingedients: [Ingedients]
var steps: [Steps]
var image : Image {
Image(imageName)
}
enum Category: String, CaseIterable, Codable {
case forest = "Forest fruit"
case garden = "Garden fruit"
case egzotic = "Exotic"
case vegatble = "Vegetables"
}
private var imageName: String
struct Steps: Hashable, Codable {
var id: Int
var description: String
}
struct Ingedients: Hashable, Codable {
var id: Int
var name: String
var quantity: Double
var unit: String
}
}
And now I builded form view with first few fields:
struct SmoothieForm: View {
var body: some View {
VStack {
Text("Add smooth")
HStack {
Text("Name")
TextField("Placeholder", text: .constant(""))
}
HStack {
Text("Category")
TextField("Placeholder", text: .constant(""))
}
HStack {
Text("Time")
TextField("Placeholder", text: .constant(""))
}
Divider()
}
.padding(.all)
}
}
struct SmoothieForm_Previews: PreviewProvider {
static var previews: some View {
SmoothieForm()
}
}
Class for load data from json :
import Foundation
final class ModelData:ObservableObject{
#Published var smoothies: [Smoothie] = load("smoothieData.json")
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename,withExtension: nil) else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
I work with c # on a daily basis
import SwiftUI
//You need default values so you can initialize an empyty item
struct Smoothie : Hashable, Codable, Identifiable {
//Find a way to make this unique maybe switch to UUID
var id: Int = 999999
var name: String = ""
var category: Category = Category.unknown
var wasDone: Bool = false
var isFavorite: Bool = false
var time: String = ""
var ingedients: [Ingedients] = []
var steps: [Steps] = []
var image : Image {
if !imageName.isEmpty{
return Image(imageName)
}else{
return Image(systemName: "photo")
}
}
enum Category: String, CaseIterable, Codable {
case forest = "Forest fruit"
case garden = "Garden fruit"
case egzotic = "Exotic"
case vegatble = "Vegetables"
case unknown
}
private var imageName: String = ""
struct Steps: Hashable, Codable {
var id: Int
var description: String
}
struct Ingedients: Hashable, Codable {
var id: Int
var name: String
var quantity: Double
var unit: String
}
}
struct SmothieForm: View {
//Give the View access to the Array
#StateObject var vm: ModelData = ModelData()
//Your new smoothie will be an empty item
#State var newSmoothie: Smoothie = Smoothie()
var body: some View {
VStack {
Text("Add smooth")
HStack {
Text("Name")
//reference the new smoothie .constant should only be used in Preview Mode
TextField("Placeholder", text: $newSmoothie.name)
}
VStack {
Text("Category")
//reference the new smoothie .constant should only be used in Preview Mode
Picker(selection: $newSmoothie.category, label: Text("Category"), content: {
ForEach(Smoothie.Category.allCases, id: \.self){ category in
Text(category.rawValue).tag(category)
}
})
}
HStack {
Text("Time")
//reference the new smoothie .constant should only be used in Preview Mode
TextField("Placeholder", text: $newSmoothie.time)
}
Divider()
//Append to array when the user Saves
Button("Save - \(vm.smoothies.count)", action: {
vm.smoothies.append(newSmoothie)
})
}
.padding(.all)
}
}

Resources