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

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.

Related

Refresh picker selection when published variable is reset

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.

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 / Combine : Listening array items value change

I want to display multiple text fields, representing scores of each part of a match.
Example : For a volleyball match, we have 25/20, 25/22, 25/23. The global score is 3/0.
The global components architecture :
>> ParentComponent
>> MainComponent
>> X TextFieldsComponent (2 text fields, home/visitor score)
The lowest component, TextFieldsComponent, contains basic bindings :
struct TextFieldsComponent: View {
#ObservedObject var model: Model
class Model: ObservableObject, Identifiable, CustomStringConvertible {
let id: String
#Published var firstScore: String
#Published var secondScore: String
var description: String {
"\(firstScore) \(secondScore)"
}
init(id: String, firstScore: String = .empty, secondScore: String = .empty) {
self.id = id
self.firstScore = firstScore
self.secondScore = secondScore
}
}
var body: some View {
HStack {
TextField("Dom.", text: $model.firstScore)
.keyboardType(.numberPad)
TextField("Ext.", text: $model.secondScore)
.keyboardType(.numberPad)
}
}
}
The parent component needs to show the total score of all parts of the match. And I wanted to try a Combine binding/stream to get the total score.
I tried multiple solutions and I ended up with this non-working code (the reduce seems to not be take all the elements of the array but internally stores a previous result) :
struct MainComponent: View {
#ObservedObject var model: Model
#ObservedObject private var totalScoreModel: TotalScoreModel
class Model: ObservableObject {
#Published var scores: [TextFieldsComponent.Model]
init(scores: [TextFieldsComponent.Model] = [TextFieldsComponent.Model(id: "main")]) {
self.scores = scores
}
}
private final class TotalScoreModel: ObservableObject {
#Published var totalScore: String = ""
private var cancellable: AnyCancellable?
init(publisher: AnyPublisher<String, Never>) {
cancellable = publisher.print().sink {
self.totalScore = $0
}
}
}
init(model: Model) {
self.model = model
totalScoreModel = TotalScoreModel(
publisher: Publishers.MergeMany(
model.scores.map {
Publishers.CombineLatest($0.$firstScore, $0.$secondScore)
.map { ($0.0, $0.1) }
.eraseToAnyPublisher()
}
)
.reduce((0, 0), { previous, next in
guard let first = Int(next.0), let second = Int(next.1) else { return previous }
return (
previous.0 + (first == second ? 0 : (first > second ? 1 : 0)),
previous.1 + (first == second ? 0 : (first > second ? 0 : 1))
)
})
.map { "[\($0.0)] - [\($0.1)]" }
.eraseToAnyPublisher()
)
}
var body: some View {
VStack {
Text(totalScoreModel.totalScore)
ForEach(model.scores) { score in
TextFieldsComponent(model: score)
}
}
}
}
I'm searching for a solution to get an event on each binding change, and merge it in a single stream, to display it in MainComponent.
N/B: The TextFieldsComponent needs to be usable in standalone too.
MergeMany is the correct approach here, as you started out yourself, though I think you overcomplicated things.
If you want to display the total score in the View (and let's say the total score is "owned" by Model instead of TotalScoreModel, which makes sense since it owns the underlying scores), you'd then need to signal that this model will change when any of the underlying scores will change.
Then you can provide the total score as a computed property, and SwiftUI will read the updated value when it recreates the view.
class Model: ObservableObject {
#Published var scores: [TextFieldsComponent.Model]
var totalScore: (Int, Int) {
scores.map { ($0.firstScore, $0.secondScore) }
.reduce((0,0)) { $1.0 > $1.1 ? ( $0.0 + 1, $0.1 ) : ($0.0, $0.1 + 1) }
}
private var cancellables = Set<AnyCancellable>()
init(scores: [TextFieldsComponent.Model] = [.init(id: "main")]) {
self.scores = scores
// get the ObservableObjectPublisher publishers
let observables = scores.map { $0.objectWillChange }
// notify that this object will change when any of the scores change
Publishers.MergeMany(observables)
.sink(receiveValue: self.objectWillChange.send)
.store(in: &cancellables)
}
}
Then, in the View, you can just use the Model.totalScore as usual:
#ObservedObject var model: Model
var body: some View {
Text(model.totalScore)
}

SwiftUI example for autocompletion

I'm a SwiftUI beginner. I have an array of "values" provided by an API and what I want is to make autocompletion when we tap characters in a "textfield". Can you please provide me an example of code for SwiftUI which can do this stuff ?
What I mean by autocompletion is this :
I have my own values and not those provided by google such here;
thx
The code from this repository used: https://github.com/simonloewe/TextFieldInputPrediction
And modified so the predictions are returned as a list like this:
//
// ContentView.swift
// StackOverflow
//
// Created by Simon Löwe on 04.04.20.
// Copyright © 2020 Simon Löwe. All rights reserved.
//
import SwiftUI
struct ContentView: View {
#State var textFieldInput: String = ""
#State var predictableValues: Array<String> = ["First", "Second", "Third", "Fourth"]
#State var predictedValue: Array<String> = []
var body: some View {
VStack(alignment: .leading){
Text("Predictable Values: ").bold()
HStack{
ForEach(self.predictableValues, id: \.self){ value in
Text(value)
}
}
PredictingTextField(predictableValues: self.$predictableValues, predictedValues: self.$predictedValue, textFieldInput: self.$textFieldInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
// This is the only modification from the example in the repository
List() {
ForEach(self.predictedValue, id: \.self){ value in
Text(value)
}
}
}.padding()
}
}
/// TextField capable of making predictions based on provided predictable values
struct PredictingTextField: View {
/// All possible predictable values. Can be only one.
#Binding var predictableValues: Array<String>
/// This returns the values that are being predicted based on the predictable values
#Binding var predictedValues: Array<String>
/// Current input of the user in the TextField. This is Binded as perhaps there is the urge to alter this during live time. E.g. when a predicted value was selected and the input should be cleared
#Binding var textFieldInput: String
/// The time interval between predictions based on current input. Default is 0.1 second. I would not recommend setting this to low as it can be CPU heavy.
#State var predictionInterval: Double?
/// Placeholder in empty TextField
#State var textFieldTitle: String?
#State private var isBeingEdited: Bool = false
init(predictableValues: Binding<Array<String>>, predictedValues: Binding<Array<String>>, textFieldInput: Binding<String>, textFieldTitle: String? = "", predictionInterval: Double? = 0.1){
self._predictableValues = predictableValues
self._predictedValues = predictedValues
self._textFieldInput = textFieldInput
self.textFieldTitle = textFieldTitle
self.predictionInterval = predictionInterval
}
var body: some View {
TextField(self.textFieldTitle ?? "", text: self.$textFieldInput, onEditingChanged: { editing in self.realTimePrediction(status: editing)}, onCommit: { self.makePrediction()})
}
/// Schedules prediction based on interval and only a if input is being made
private func realTimePrediction(status: Bool) {
self.isBeingEdited = status
if status == true {
Timer.scheduledTimer(withTimeInterval: self.predictionInterval ?? 1, repeats: true) { timer in
self.makePrediction()
if self.isBeingEdited == false {
timer.invalidate()
}
}
}
}
/// Capitalizes the first letter of a String
private func capitalizeFirstLetter(smallString: String) -> String {
return smallString.prefix(1).capitalized + smallString.dropFirst()
}
/// Makes prediciton based on current input
private func makePrediction() {
self.predictedValues = []
if !self.textFieldInput.isEmpty{
for value in self.predictableValues {
if self.textFieldInput.split(separator: " ").count > 1 {
self.makeMultiPrediction(value: value)
}else {
if value.contains(self.textFieldInput) || value.contains(self.capitalizeFirstLetter(smallString: self.textFieldInput)){
if !self.predictedValues.contains(String(value)) {
self.predictedValues.append(String(value))
}
}
}
}
}
}
/// Makes predictions if the input String is splittable
private func makeMultiPrediction(value: String) {
for subString in self.textFieldInput.split(separator: " ") {
if value.contains(String(subString)) || value.contains(self.capitalizeFirstLetter(smallString: String(subString))){
if !self.predictedValues.contains(value) {
self.predictedValues.append(value)
}
}
}
}
}
Provides the following outcome:
Tested on Version 11.5 and iOS 13.5

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