Refresh picker selection when published variable is reset - swift

I have a published variable that keeps track in this case, an amount of time. When a user saves their values, the variable will be reset (in this case back to an empty string. My recipeClass is updated correctly (as I can see my variable updating to the users selection, then back to "" when submitted. I'll include the published variable below here for reference:
#Published var recipePrepTime: String = ""
My Picker in the view is just not updating back to the initial value, in this case "5 minutes" How can I accomplish this? Any feedback would be great. Please find my code below:
struct RecipeEditorView: View {
#ObservedObject var recipeClass: Recipe
//#State var recipeTitle = ""
#State private var recipeTime = "Cook Time"
#State private var pickerTime: String = ""
//calls macro pickers
var cookingTime = ["5 Mins", "10 Mins","15 Mins","20 Mins","25 Mins","30 Mins ","45 Mins ","1 Hour","2 Hours", "A Long Time", "A Very Long Time"]
var body: some View {
HStack(spacing: 0){
ZStack{
Image(systemName:("clock"))
.padding(.leading, 150)
.foregroundColor(Color("completeGreen"))
//picker I would like updated
Picker(selection: $recipeClass.recipePrepTime, label: Text("")) {
ForEach(cookingTime, id: \.self) {
Text($0)
}
}
.onChange(of: pickerTime, perform: { _ in
recipeClass.recipePrepTime = pickerTime
})
.accentColor(.gray)
}
.multilineTextAlignment(.center)
}
}
}
}
Noting I didn't include my recipeClass as the values are being correctly updated on the backend.

Related

Making variable for each iteration in foreach loop SwiftUI

I have a nestled foreach loop and I want to modify data for each element in the nestled loop. Like this:
struct trainingView: View {
#ObservedObject var workout: Workout
#State private var reps: Int = 0
var body: some View {
ForEach(workout.exercises, id: \.self) { exercise in
Text(exercise.nameOfExercise)
ForEach(0..<exercise.amountOfSets, id: \.self) { Set in
Text("\(Set+1) Set")
Text(reps)
Stepper("Reps", value: $reps , in: 0...30)
}
}
}
}
I would like the variable rep to be independent from changes in an another elements stepper. If you need better examples of the code just ask.
This does not work as one change affects all elements. I have also tried making an #State object in the nestled loop but this doesn't work either. I have also tried making an array to store the data but this seemed complicated since the size of the array can change from different times. I have tried to search for the information on here but haven't found anyone answering it. I am fairly new to swiftUI and in need of help.
The Stepper in your example takes a Binding to a numeric value. If you create an array-of-arrays, you can create a Binding by accessing each row and element within it via their indices, e.g.:
struct Exercise: Identifiable, Equatable, Hashable {
let id = UUID()
let name: String
var sets: [Set]
}
struct Set: Identifiable, Equatable, Hashable {
let id = UUID()
let name: String
let requiredReps: Int = 10
var reps: Int = 0
}
class Workout: ObservableObject {
#Published var exercises: [Exercise]
init(exercises: [Exercise]) {
self.exercises = exercises
}
}
struct ContentView: View {
#StateObject var workout = Workout(exercises: [
Exercise(name: "Squats", sets: [Set(name: "Set 1"), Set(name: "Set 2"), Set(name: "Set 3")]),
Exercise(name: "Lunges", sets: [Set(name: "Set 1"), Set(name: "Set 2"), Set(name: "Set 3")]),
Exercise(name: "Burpees", sets: [Set(name: "Set 1"), Set(name: "Set 2"), Set(name: "Set 3")])
])
var body: some View {
List {
ForEach(workout.exercises) { exercise in
Section(exercise.name) {
let rowIndex = workout.exercises.firstIndex(of: exercise)!
HStack {
ForEach(exercise.sets) { set in
let objectIndex = exercise.sets.firstIndex(of: set)!
VStack {
Text(set.name + ": ") + Text(set.reps, format: .number) + Text("/") + Text(set.requiredReps, format: .number)
Stepper("Reps", value: $workout.exercises[rowIndex].sets[objectIndex].reps, in: 0...set.requiredReps)
.labelsHidden()
}
}
}
}
}
.padding()
}
}
}

SwiftUI's List displays the same row twice in ForEach [closed]

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed 9 months ago.
Improve this question
can anyone please help me to solve this? Please paste the same code in your Xcode and run it. Please follow the below steps:
Select Date as 1 June 2022, Time 1 as 07:00 and Time2 as 15:00. Enter load1 as 7 and load2 as 8. Click on Add load. It will show up the date, time1 and time2 with total amount.
Similarly keeping the time same(07:00-15:00) and changing the dates to 3rd of June and 4th of June and with different values of load1 and load2, click on Add load.
Now click on the Edit button and delete the data of 4th June and 1st June and then Enter a new data using 2nd June. The date and total amount of 3rd June automatically changes to 2nd of June. So I now have both data as 2nd June. Also the Total comes up with a different value. Why is this happening? any idea where am I doing wrong here?
struct Task : Identifiable {
var id = String()
var toDoItem = String()
var amount : Float = 0 //<-- Here
}
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
}
struct Calculation: View {
#State var load1 = Float()
#State var load2 = Float()
#State var gp : Float = 0
#State var rate: Float = 0
#ObservedObject var taskStore = TaskStore()
#State private var birthDate = Date()
#State private var time1 = Date()
#State private var time2 = Date()
func addNewToDo() {
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
let dayoftime = formatter.string(from: birthDate)
let formatter2 = DateFormatter()
formatter2.dateFormat = "HH:mm"
let timeof1 = formatter2.string(from: time1)
let timeof2 = formatter2.string(from: time2)
taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), toDoItem: "\(dayoftime) \(timeof1)-\(timeof2) total = \(rate.description) ", amount: rate))
//gp += rate
}
var body: some View {
NavigationView {
VStack {
HStack {
VStack(spacing: 1) {
Section(header: Text("Date") .foregroundColor(.black)
){
DatePicker(selection: $birthDate, displayedComponents: [.date]){Text(" Time")
}
}
VStack(spacing: 1) {
Section(header: Text("Time1") .foregroundColor(.black)){
DatePicker(selection: $time1, displayedComponents: [.hourAndMinute]){Text(" Time 1")
}
}
VStack(spacing: 1) {
Section(header: Text("Time2") .foregroundColor(.black)
){ DatePicker(selection: $time2, displayedComponents: [.hourAndMinute]){Text(" Time 1")
}
}
}
List {
Section(header:Text("load 2"))
{
TextField("Enter value of load 1", value: $load1, format: .number)
TextField("Enter value of load 1", value: $load2, format: .number)
}
HStack {
Button(String(format: "Add Load"), action: {
print(Rocky(mypay: rate))
gp += rate
})
Button(action: {
addNewToDo()
Rocky(mypay: rate)
},
label: {
Text(" ")
})
}
ForEach(self.taskStore.tasks) { task in
Text(task.toDoItem)
}
.onMove(perform : self.move)
.onDelete(perform : self.delete) //For each
}
.navigationBarTitle("SHIFTS")
.navigationBarItems(trailing: EditButton()) //List
Text("Total = $\(gp) ")
}.onAppear()
}
}
}
}
}
func Rocky(mypay: Float)
{
rate = load1 + load2
print("Sus \(gp)")
}
func move(from source : IndexSet, to destination : Int)
{
taskStore.tasks.move(fromOffsets: source, toOffset: destination)
}
func delete(at offsets : IndexSet) {
if let index = offsets.first { //<-- Here
let task = taskStore.tasks[index]
gp -= task.amount
}
taskStore.tasks.remove(atOffsets: offsets)
}
}
The issue is that ForEach needs your items to be uniquely identifiable.
You're using an autogenerated id for your tasks that is basically calculated as a current count of tasks in the storage:
taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), ......`
So here's what's happening:
You add first three tasks, they get the IDs of "1", "2" and "3".
You remove the tasks with the IDs "1" and "3". The only task that 's remaining in the array is the one with the ID of "2".
You're adding another task to the list. When creating a task, the count of the tasks in the array is 1, so the new task gets the ID of "2". So now you have two different tasks in the array with the same IDs.
When SwiftUI renders you view, the ForEach method distinguishes your tasks by ID. It sees that it should render two rows of the list and both of them have the ID of "2". So it finds the first item with the ID equal to "2" and renders it twice because it assumes that all items in the array have unique IDs (which the should have really).
To solve this, use a UUID as an identifier for a task.
You can also create an initializer that doesn't take an ID as an argument because the task will create a unique identifier for itself:
struct Task: Identifiable {
var id: UUID
var toDoItem: String
var amount: Float
init(toDoItem: String, amount: Float) {
self.id = UUID()
self.toDoItem = toDoItem
self.amount = amount
}
}
When creating a new task, use this new initializer without assigning items an explicit identifier:
taskStore.tasks.append(
Task(
toDoItem: "\(dayoftime) \(timeof1)-\(timeof2) total = \(rate.description) ",
amount: rate
)
)
That's it, now all tasks will have unique identifiers and will render correctly.

Call viewModel in Picker selection

I have an observedObject that calls to a viewModel (variable vm). The viewModel contains all the user's information already saved. I want to use the value that the user already saved as the selection of the picker. If no data exists yet for the user (in this case their height, I just leave the selection as an empty string.
I'm getting a bunch of issues with my current approach and can't seem to figure it out. The errors include:
Cannot convert value of type 'String' to expected argument type
'Binding'
Add explicit 'self.' to refer to mutable property of
'PersonalSettingsView'
Chain the optional using '?' to access member 'weight' only for
non-'nil' base values
struct PersonalSettingsView: View {
#ObservedObject var vm = DashboardLogic() //call to viewModel
#State private var height: String = ""
private var heightOptions = ["4'0", "4'1","4'2","4'3", "4'4", "4'5","4'6","4'7","4'8","4'9","4'10","4'11","5'0","5'1", "5'2", "5'3", "5'4", "5'5","5'6","5'7","5'8","5'9","5'10","5'11","6'0","6'1","6'2","6'3","6'4","6'5","6'6","6'7","6'8","6'9","6'10","6'11","7'0","7'1","7'2"]
var body: some View {
NavigationView{
Form{
Section(header: Text("Health Stats")
.foregroundColor(.black)
.font(.body)
.textCase(nil)){
HStack{
Picker(selection: $vm.userModel?.height ?? "", label: Text("Height")){
ForEach(heightOptions, id: \.self){ height in
Text(height)
}
}
}
}
}
}
}
}
you could try this approach, it's a bit convoluted, but it works for me:
HStack{
if vm.userModel != nil {
Picker(selection: Binding<String> (
get: { vm.userModel!.height },
set: { vm.userModel!.height = $0 }),
label: Text("Height")) {
ForEach(heightOptions, id: \.self){ height in
Text(height)
}
}
}
}
Also add this to the Form:
.onAppear {
if vm.userModel == nil { vm.userModel = User(height: "") }
}
If vm.userModel is nil, there is no user to set the height of,
no matter what you select.
So you need to create a user when vm.userModel is nil.
Then you can make a selection for that empty user. You can do this in
.onAppear{ ... } for example.

Generic parameter 'Parent' could not be inferred

I'm working on my first Swift/SwiftUI project (other than the tutorials) and have run into what I think is a common problem -- the error Generic parameter 'Parent' could not be inferred -- toward the top of the view when the problem is actually with a List I'm trying to generate lower down in the form.
The app I'm building is a simple invoicing app: the user fills out the form fields and sends the invoice. An invoice can have multiple line items that the user enters one at a time and then are appended to a dictionary that should display inside the form.
This is the relevant variables from the top of the struct and the beginning of the view, where I hope I'm declaring the variables for line items correctly to modify them based on user input.
*Edited following #asperi's advice below.
#State private var lineItems = [LineItem]()
#State private var lineItem = LineItem()
struct LineItem: Codable, Hashable {
var productSku: String = ""
var productName: String = ""
var quantity: Double = 0
var unitPrice: Double = 0
}
func addLineItem(lineItem: LineItem){
lineItems.append(lineItem)
}
...
var body: some View {
NavigationView {
Form {
Section(header: Text("Customer Information")) { <-- error appears here
TextField("Customer Name", text: $customerName)
TextField("Customer Email", text: $customerEmail)
}
Here's the relevant portion of the form, where I'm trying to list all current line items and then allow the user to insert additional line items. I don't get any error until I add the List code, so I'm pretty sure I'm doing something wrong there.
Section(header: Text("Items")) {
List(lineItems, id: \.self) { item in
LineItemRow(lineItem: item)
Text(item.productName)
}
TextField("Product SKU", text: $productSKU)
TextField("Poduct Name", text: $productName)
TextField("Unit Price", text: $unitPrice, formatter: DoubleFormatter())
Picker("Quantity", selection: $quantity) {
ForEach(0 ..< 10) {
Text("\($0)")
}
}
Button(action: {
self.addLineItem(lineItem: LineItem(productSku:$productSKU,
productName:$productName,
quantity:$unitPrice,
unitPrice:$quantity))
print($lineItems)
}, label: {
Text("Add line item")
})
}
I've tested the button functionality in the console and it does seem to be appending to the dictionary correctly, but I need it to display as well.
I'm probably getting something very basic wrong with the List. Any advice?
For reference, here's the whole view:
struct AddInvoiceForm: View {
#State private var invoiceNumber: String = ""
#State private var _description: String = ""
#State private var dueDate: Date = Date()
#State private var sendImmediately: Bool = true
#State private var currency = 0
#State private var paymentType = 0
#State private var customerName: String = ""
#State private var customerEmail: String = ""
#State private var productSKU: String = ""
#State private var productName: String = ""
#State private var quantity: Int = 0
#State private var unitPrice: String = ""
#State private var lineItems = [LineItem]()
#State private var lineItem = LineItem()
struct LineItem: Codable, Hashable {
var productSku: String = ""
var productName: String = ""
var quantity: Double = 0
var unitPrice: Double = 0
}
func addLineItem(lineItem: LineItem){
lineItems.append(lineItem)
}
#State private var totalAmount: Double = 0.0
static let currencies = ["USD","GBP"]
var body: some View {
NavigationView {
Form {
Section(header: Text("Customer Information")) {
TextField("Customer Name", text: $customerName)
TextField("Customer Email", text: $customerEmail)
}
Section(header: Text("Invoice Information")) {
TextField("Invoice Number", text:$invoiceNumber)
TextField("Description", text:$_description)
DatePicker(selection: $dueDate, in: Date()..., displayedComponents: .date) {
Text("Due date")
}
Picker("Currency", selection: $currency) {
ForEach(0 ..< Self.currencies.count) {
Text(Self.currencies[$0])
}
}
}
Section(header: Text("Items")) {
List(lineItems, id: \.self) { item in
LineItemRow(lineItem: item)
Text(item.productName)
}
TextField("Product SKU", text: $productSKU)
TextField("Poduct Name", text: $productName)
TextField("Unit Price", text: $unitPrice, formatter: DoubleFormatter())
Picker("Quantity", selection: $quantity) {
ForEach(0 ..< 10) {
Text("\($0)")
}
}
Button(action: {
self.addLineItem(lineItem: LineItem(productSku:$productSKU,
productName:$productName,
quantity:$unitPrice,
unitPrice:$quantity))
print($lineItems)
}, label: {
Text("Add line item")
})
}
Section(header: Text("Totals")) {
Text("\(totalAmount)")
}
Section {
Button(action: {
print(Text("Send"))
}, label: {
Text("Send invoice")
})
}
}.navigationBarTitle("Invoice Details")
}
}
}
List(lineItems, id: \.productSku) { item in <-- I get the error when I add this
Your item is dictionary, but dictionary does not have .productSku key path, so the error.
I assume most simple & correct would be to make Item as struct
struct LineItem {
var productSku: String
...
}
...
#State private var lineItems = [LineItem]()
#State private var lineItem = LineItem()
...
List(lineItems, id: \.productSku) { item in

Why does picker binding not update when using SwiftUI?

I have just begun learning Swift (and even newer at Swift UI!) so apologies if this is a newbie error.
I am trying to write a very simple programme where a user chooses someone's name from a picker and then sees text below that displays a greeting for that person.
But, the bound var chosenPerson does not update when a new value is picked using the picker. This means that instead of showing a greeting like "Hello Harry", "Hello no-one" is shown even when I've picked a person.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson: String? = nil
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach ((0..<people.count), id: \.self) { person in
Text(self.people[person])
}
}
}
Section{
Text("Hello \(chosenPerson ?? "no-one")")
}
}
}
}
}
(I have included one or two pieces of the original formatting in case this is making a difference)
I've had a look at this question, it seemed like it might be a similar problem but adding .tag(person) to Text(self.people[person])did not solve my issue.
How can I get the greeting to show the picked person's name?
Bind to the index, not to the string. Using the picker, you are not doing anything that would ever change the string! What changes when a picker changes is the selected index.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson = 0
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach(0..<people.count) { person in
Text(self.people[person])
}
}
}
Section {
Text("Hello \(people[chosenPerson])")
}
}
}
}
}
The accepted answer is right if you are using simple arrays, but It was not working for me because I was using an array of custom model structs with and id defined as string, and in this situation the selection must be of the same type as this id.
Example:
struct CustomModel: Codable, Identifiable, Hashable{
var id: String // <- ID of type string
var name: String
var imageUrl: String
And then, when you are going to use the picker:
struct UsingView: View {
#State private var chosenCustomModel: String = "" //<- String as ID
#State private var models: [CustomModel] = []
var body: some View {
VStack{
Picker("Picker", selection: $chosenCustomModel){
ForEach(models){ model in
Text(model.name)
.foregroundColor(.blue)
}
}
}
Hope it helps somebody.