Iterating an object array and changing values? - swiftui

Used this scenario as an example to be a bit more self explanatory. I have a struct which represents a character, and one of the structs attributes is another struct: Stats (where I used id to represent the name in a more simple way).
Besides that I have a view with a ForEach, where I iterate over some Characters and I need to be able to increase a specific stat.
The problem is: I'm trying to increase stat.points using a button, but I keep getting the message "Left side of mutating operator isn't mutable: 'product' is a 'let' constant".
struct Character: Identifiable {
var id: String
var name: String
var stats: [Stats]
}
struct Stats: Identifiable {
var id: String
var points: Int
}
ForEach(characters.stats) { stat in
HStack {
Text("\(stat.id)")
Button {
stat.points += 1
} label: {
Text("Increase")
}
}
}
How could I make this work?

As you have found, structs in your stats: [Stats] does not allow changes
especially in a ForEach where it is a let.
There are many ways to achieve what you want, depending on what you trying to do.
This example code shows the most basic way, using the array of stats: [Stats]
directly:
struct ContentView: View {
#State var characters = Character(id: "1",name: "name-1", stats: [Stats(id: "1", points: 1),Stats(id: "2", points: 1)])
var body: some View {
ForEach(characters.stats.indices, id: \.self) { index in
HStack {
Text("\(characters.stats[index].id)")
Button {
characters.stats[index].points += 1
} label: {
Text("Increase")
}
Text("\(characters.stats[index].points)")
}
}
}
}
Here is another approach, using a function, to increase your stat points value:
struct ContentView: View {
#State var characters = Character(id: "1", name: "name-1", stats: [Stats(id: "1", points: 1), Stats(id: "2", points: 1)])
var body: some View {
ForEach(characters.stats) { stat in
HStack {
Text("\(stat.id)")
Button {
increase(stat: stat)
} label: {
Text("Increase")
}
Text("\(stat.points)")
}
}
}
func increase(stat: Stats) {
if let index = characters.stats.firstIndex(where: {$0.id == stat.id}) {
characters.stats[index].points += 1
}
}
}

Related

Does SwiftUI's ForEach cache #State variables of child views beyond their existence?

So here is a little piece of code that sums up a problem I cannot figure out atm.
In the code below I add and remove entries to a dictionary keyed by an Enum.
What I would expect is that every time I add an item a new random number is being generated in the Element view and displayed.
What happens is that for every same Ident the same random number shows up - event though the ForEach loop has had a state where that Ident key was not in the dictionary any more. It appears as if ForEach does not purge the #State vars of the Element views that are not present any more, but reuses them when a new entry to the dictionary is added with the same Ident.
Is this expected behavior? What am I doing wrong?
Here is the code:
import Foundation
import SwiftUI
enum Ident:Int, Comparable, CaseIterable {
case one=1, two, three, four
static func < (lhs: Ident, rhs: Ident) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
extension Dictionary where Key == Ident,Value== String {
var asSortedArray:Array<(Ident,Value)> {
Array(self).sorted(by: { $0.key < $1.key })
}
var nextKey:Ident? {
if self.isEmpty {
return .one
}
else {
return Array(Set(Ident.allCases).subtracting(Set(self.keys))).sorted().first
}
}
}
struct ContentView: View {
#State var dictionary:[Ident:String] = [:]
var body: some View {
Form {
Section {
ForEach(dictionary.asSortedArray, id: \.0) { (ident, text) in
Element(dictionary: $dictionary, ident: ident, text: text)
}
}
Section {
Button(action: {
if let nextIdent = dictionary.nextKey {
dictionary[nextIdent] = "Something"
}
}, label: {
Text("Add one")
})
}
}
}
}
struct Element:View {
#Binding var dictionary:[Ident:String]
var ident:Ident
var text:String
#State var random:Int = Int.random(in: 0...1000)
var body: some View {
HStack {
Text(String(ident.rawValue))
Text(String(random))
Button(action: {
dictionary.removeValue(forKey: ident)
}, label: {
Text("Delete me.")
})
Spacer()
}
}
}

dynamic list with sections producing strange behaviour / crashes

OK, I nailed down the problem to the dynamic list with sections, so I changed the title and I'm going to shorten my post...
In my project, there is a complex view that contains a dynamic list with sections that depend on many state variables.
Problem: after a few user interactions and subsequent changes to the list, I see strange behaviour:
sometimes a section doesn't show up although the condition of the if-clause in which it is embedded is definitely met
sometimes, the section header label is replaced with the label of a list item
sometimes, it crashes
when the view is broken and I enforce a refresh of the view without changing any of the state variables, everything looks good again
The code below is taken from my project, however, greatly simplified by removing parts that are not necessary to produce the error. The state variable altered by user interaction is pIdStatus. From pIdStatus, the function data.analyze() calculates the arrays pastriesWithStatus1, pastriesWithStatus2 and pastriesWithStatus3.
Interestingly, when I remove the tab-view in which everything is embedded, everything works fine.
Any help is greatly appreciated.
import SwiftUI
struct ContentView: View {
#StateObject var data = Data()
var body: some View {
TabView { // needed in my project and needed for crash.
List {
// for debugging purpose: create a button that triggers a refresh
Button {
data.counter += 1
} label: {
HStack() {
Text("Refresh")
.font(.system(size: 16, weight: .regular))
}
}
// Section with status3-items
if data.pastriesWithStatus3.count > 0 {
Section(header:
HStack() {
Text("STATUS 3")
}
){
ForEach(data.pastriesWithStatus3, id: \.self) { i in
subview21(data: data, label: data.pastries.first(where: { $0.id == i })!.name, id: i, status: 3)
}
}
}
// section with other items
Section(header:
HStack() {
Text("OTHER ITEMS")
}
){
ForEach(data.pastriesWithStatus2, id: \.self) { i in
subview21(data: data, label: data.pastries.first(where: { $0.id == i })!.name, id: i, status: 2)
}
ForEach(data.pastriesWithStatus1, id: \.self) { i in
subview21(data: data, label: data.pastries.first(where: { $0.id == i })!.name, id: i, status: 1)
}
}
}
.listStyle(InsetGroupedListStyle())
.tabItem {
Image(systemName: "cart")
Text("shopping")
}.onAppear {
data.analyze()
}
}
}
}
struct subview21: View {
#ObservedObject var data : Data
let hspacing: CGFloat = 20
let tab1: CGFloat = 150
var label: String
var id: Int
var status: Int // 1=red, 2=unchecked, 3=green
var body: some View {
if data.testcondition { // this line is required for crash. Although it is always true.
HStack (spacing: hspacing) {
Text(data.pastries.first(where: { $0.id == id })!.name)
.frame(width: tab1, alignment: .leading)
Button {
data.pIdStatus[id] = 1
data.analyze()
} label: {
Image(systemName: (status == 1 ? "checkmark.square.fill" : "square"))
.foregroundColor(.red)
}
.buttonStyle(BorderlessButtonStyle())
Button {
data.pIdStatus[id] = 3
data.analyze()
} label: {
Image(systemName: (status == 3 ? "checkmark.square.fill" : "square"))
.foregroundColor(.green)
}
.buttonStyle(BorderlessButtonStyle())
}
}
}
}
let maxPId = 99
struct Person: Identifiable, Hashable {
let id: Int
let name: String
var orders: [Order]
}
struct Pastry: Identifiable, Hashable {
let id: Int
let name: String
}
struct Order: Hashable {
var pastryId: Int
var n: Int
var otherwise: OrderL2?
}
struct OrderL2: Hashable {
let level: Int = 2
var pastryId: Int
var n: Int
}
class Data : ObservableObject {
#Published var persons: [Person] = [
Person(id: 1, name: "John", orders: [Order(pastryId: 0, n:1, otherwise:OrderL2(pastryId: 1, n:2))]),
]
#Published var pastries: [Pastry] = [Pastry(id: 0, name: "Donut"),
Pastry(id: 1, name: "prezel")]
#Published var counter: Int = 0 // for debugging
#Published var pIdStatus: [Int] = Array(repeating: 2, count: maxPId)
#Published var testcondition: Bool = true // needed in my project, for simplicity, here, always true
#Published var pastriesWithStatus1: [Int] = []
#Published var pastriesWithStatus2: [Int] = []
#Published var pastriesWithStatus3: [Int] = []
func analyze() {
var localPastriesWithStatus1: [Int] = []
var localPastriesWithStatus2: [Int] = []
var localPastriesWithStatus3: [Int] = []
for p in pastries { // there may be more elegant ways to code, however, in real life, there's a lot more functionality here
if p.id < maxPId {
if pIdStatus[p.id] == 1 {
localPastriesWithStatus1.append(p.id)
}
if pIdStatus[p.id] == 2 {
localPastriesWithStatus2.append(p.id)
}
if pIdStatus[p.id] == 3 {
localPastriesWithStatus3.append(p.id)
}
}
}
pastriesWithStatus1 = localPastriesWithStatus1
pastriesWithStatus2 = localPastriesWithStatus2
pastriesWithStatus3 = localPastriesWithStatus3
}
}
Apparently, swiftui List is not made for Lists that are built from scratch each time the view is refreshed. Instead, Lists must be linked to arrays to/from which items are added/removed one by one.
I solved the problem by avoiding List altogether, instead using Stack, Text etc.

Using TextField with ForEach in SwiftUI

I am trying to display a dynamic list of text fields using a ForEach. The following code is working as expected: I can add/remove text fields, and the binding is correct. However, when I move the items in a ObservableObject view model, it does not work anymore and it crashes with an index out of bounds error. Why is that? How can I make it work?
struct ContentView: View {
#State var items = ["A", "B", "C"]
var body: some View {
VStack {
ForEach(items.indices, id: \.self) { index in
FieldView(value: Binding<String>(get: {
items[index]
}, set: { newValue in
items[index] = newValue
})) {
items.remove(at: index)
}
}
Button("Add") {
items.append("")
}
}
}
}
struct FieldView: View {
#Binding var value: String
let onDelete: () -> Void
var body: some View {
HStack {
TextField("item", text: $value)
Button(action: {
onDelete()
}, label: {
Image(systemName: "multiply")
})
}
}
}
The view model I am trying to use:
class ViewModel: Observable {
#Published var items: [String]
}
#ObservedObject var viewModel: ViewModel
I found many questions dealing with the same problem but I could not make one work with my case. Some of them do not mention the TextField, some other are not working (anymore?).
Thanks a lot
By checking the bounds inside the Binding, you can solve the issue:
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel(items: ["A", "B", "C"])
var body: some View {
VStack {
ForEach(viewModel.items.indices, id: \.self) { index in
FieldView(value: Binding<String>(get: {
guard index < viewModel.items.count else { return "" } // <- HERE
return viewModel.items[index]
}, set: { newValue in
viewModel.items[index] = newValue
})) {
viewModel.items.remove(at: index)
}
}
Button("Add") {
viewModel.items.append("")
}
}
}
}
It is a SwiftUI bug, similar question to this for example.
I can not perfectly explain what is causing that crash, but I've been able to reproduce the error and it looks like after deleting a field,SwiftUI is still looking for all indices and when it is trying to access the element at a deleted index, it's unable to find it which causes the index out of bounds error.
To fix that, we can write a conditional statement to make sure an element is searched only if its index is included in the collection of indices.
FieldView(value: Binding<String>(get: {
if viewModel.items.indices.contains(index) {
return viewModel.items[index]
} else {
return ""
}
}, set: { newValue in
viewModel.items[index] = newValue
})) {
viewModel.items.remove(at: index)
}
The above solution solves the problem since it makes sure that the element will not be searched when the number of elements (items.count) is not greater than the index.
This is just what I've been able to understand, but something else might be happening under the hood.

Even and Odd rows using ForEach (SwiftUI)

What is the best way to distinguish even and odd rows in ForEach loop? Contents of the loop is not numbers (i.e. User struct), and it can be filtered using search phrase (using just index of item in array is not applicable in that way, I think). I need to change a color of that rows.
If I understand question correctly, you can use indices of your data array and check condition index % 2 == 1 (because indices begins from 0) for odd rows. For filtered data I suggest computed value:
import SwiftUI
import Combine
struct HighlightingRowData: Identifiable {
let id = UUID()
let title: String
}
final class SomeData: ObservableObject {
#Published var data: [HighlightingRowData] = [HighlightingRowData(title: "R. Martin"), HighlightingRowData(title: "McConell"), HighlightingRowData(title: "London"), HighlightingRowData(title: "London")]
}
struct HighlitedRowsInList: View {
#EnvironmentObject var someData: SomeData
#State private var searchedText = ""
private var filteredData: [HighlightingRowData] {
return searchedText == "" ? someData.data : someData.data.filter { $0.title.contains(searchedText) }
}
var body: some View {
List {
TextField("filter", text: $searchedText)
ForEach(filteredData.indices, id: \.self) { rowIndex in
HStack {
Text(self.filteredData[rowIndex].title)
Spacer()
}
.background(rowIndex % 2 == 1 ? Color.yellow : Color.clear)
}
}
}
}
struct HighlitedRowsInList_Previews: PreviewProvider {
static var previews: some View {
HighlitedRowsInList()
.environmentObject(SomeData())
}
}
you can achieve something like this with this code:

Conditionally Text in SwiftUI depending on Array value

I want make placeholder custom style so i try to use the method of Mojtaba Hosseini in SwiftUI. How to change the placeholder color of the TextField?
if text.isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
but in my case, I use a foreach with a Array for make a list of Textfield and Display or not the Text for simulate the custom placeholder.
ForEach(self.ListeEquip.indices, id: \.self) { item in
ForEach(self.ListeJoueurs[item].indices, id: \.self){idx in
// if self.ListeJoueurs[O][O] work
if self.ListeJoueurs[item][index].isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
}
}
How I can use dynamic conditional with a foreach ?
Now I have a another problem :
i have this code :
struct EquipView: View {
#State var ListeJoueurs = [
["saoul", "Remi"],
["Paul", "Kevin"]
]
#State var ListeEquip:[String] = [
"Rocket", "sayans"
]
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices) { item in
BulleEquip(EquipName: item, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
}
}
}
struct BulleEquip: View {
var EquipName = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
VStack{
VStack{
Text("Équipe \(EquipName+1)")
}
VStack { // Added this
ForEach(self.ListeJoueurs[EquipName].indices) { index in
ListeJoueurView(EquipNamed: self.EquipName, JoueurIndex: index, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
HStack{
Button(action: {
self.ListeJoueurs[self.EquipName].append("") //problem here
}){
Text("button")
}
}
}
}
}
}
struct ListeJoueurView: View {
var EquipNamed = 0
var JoueurIndex = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
HStack{
Text("Joueur \(JoueurIndex+1)")
}
}
}
I can run the App but I have this error in console when I click the button :
ForEach, Int, ListeJoueurView> count (3) != its initial count (2). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Can someone enlighten me?
TL;DR
You need a VStack, HStack, List, etc outside each ForEach.
Updated
For the second part of your question, you need to change your ForEach to include the id parameter:
ForEach(self.ListeJoueurs[EquipName].indices, id: \.self)
If the data is not constant and the number of elements may change, you need to include the id: \.self so SwiftUI knows where to insert the new views.
Example
Here's some example code that demonstrates a working nested ForEach. I made up a data model that matches how you were trying to call it.
struct ContentView: View {
// You can ignore these, since you have your own data model
var ListeEquip: [Int] = Array(1...3)
var ListeJoueurs: [[String]] = []
// Just some random data strings, some of which are empty
init() {
ListeJoueurs = (1...4).map { _ in (1...4).map { _ in Bool.random() ? "Text" : "" } }
}
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices, id: \.self) { item in
VStack { // Added this
ForEach(self.ListeJoueurs[item].indices, id: \.self) { index in
if self.ListeJoueurs[item][index].isEmpty { // If string is blank
Text("Placeholder")
.foregroundColor(.red)
} else { // If string is not blank
Text(self.ListeJoueurs[item][index])
}
}
}.border(Color.black)
}
}
}
}
Explanation
Here's what Apple's documentation says about ForEach:
A structure that computes views on demand from an underlying collection of of [sic] identified data.
So something like
ForEach(0..2, id: \.self) { number in
Text(number.description)
}
is really just shorthand for
Text("0")
Text("1")
Text("2")
So your ForEach is making a bunch of views, but this syntax for declaring views is only valid inside a View like VStack, HStack, List, Group, etc. The technical reason is because these views have an init that looks like
init(..., #ViewBuilder content: () -> Content)
and that #ViewBuilder does some magic that allows this unique syntax.