-4

In SwiftUI I have an array of structures, and when a button is pressed I want to change a value of one of them (and have the UI automatically update).

I am using @State for the array, so I got the impression this should work, but I am running into a lot of issues where what I change doesn't reflect in the array.

I saw a similar post here, and tried a few things listed there, but none of them seem to work. Below is a simplified version of the main relevant code that uses one of the ideas from that post (using a filter).

Oddly, I discovered that even I make a completely new array and then set "items = newItems", it doesn't work. The only partial workaround I have is by making newItems, then removing all the values from items and adding in the value from newItems (which seems like a total hack).

struct Item: Codable, Identifiable {
    var id: Int
    var value: Int
}

struct ContentView: View {
    @State private var items: [Item] = []

    var body: some View {
        NavigationStack {
            List {
                ForEach (items) { item in
                // I am expecting this to be called when I update items[], however it is never called.
                }
            }
            Button("test", action: {
                updateValueOfItem(1, 100)
            }
         }
    }

    func updateValueOfItem(id: Int, value: Int)
    {
        items.indices.filter { items[$0].id == id }
        .forEach { items[$0].value = value }
        //items[0].value = value // this doesn't work either
        print("array = \(items)") // shows old value, not new one!
    }
}

Update: I read that classes and structures are treated differently so I tried changing the Item struct to a class, but it didn't help.

5
  • This is just how @State works. You can only observe the change after the view updates. I assume this is an XY Problem. What is the ultimate problem that you are trying to solve? Why do you need to observe the change immediately? Commented Oct 2, 2024 at 20:39
  • Your code is full of errors. Maybe take a few minutes to fix it? Commented Oct 2, 2024 at 21:50
  • @sweeper I don't need to observe the change there, it was just a debugging step. I need to see the result of the Item's value properly set when it is checked during the redraw of the relevant component. I've added a ForEach to show where I am using it. Actually, I just noticed that the ForEach isn't actually called when I change the items[] array via the filter. Shouldn't the view get triggered to be redrawn when I modify something in items[]? Also I tried changing that from a struct to class, but no change. Commented Oct 2, 2024 at 22:11
  • Also I wanted to note that if I add a append() and remoteLast() after I set the .value when the button is pressed, it will correctly redraw the component and have the correct value, though this is clearly a hack that I want to avoid. Commented Oct 2, 2024 at 22:19
  • 1
    @Locksleyu your demo code has syntax errors (updateValueOfItem call). It's also important how you use / create the id property of your Item entity, that's sth. your code doesn't show. Otherwise your demo code works for me and doesn't cause the described issue. So you must have a problem somewhere in your code that we can't see. Commented Oct 3, 2024 at 0:04

3 Answers 3

1

The whole point of using a @State is so that the UI reacts to changes.

Here's something a little more complete, but yet simple enough, to show how you can read and update values and the UI, given your setup.

Note the use of ForEach with a bindings to $items, which gives you a $item binding to work with.

This allows for direct update of the array structs without needing to hit a button to run a function.

The example also includes another method, where you select a specific ID from a list, then use it to locate the item in the array and update its value.

import SwiftUI

struct StateArrayItem: Hashable, Codable, Identifiable {
    let id: Int 
    var value: Int
}

struct StateArrayContentView: View {
    
    //State values
    @State private var selectedItem: StateArrayItem?
    @State private var pickerFieldValue: Int?
    @FocusState private var focusedField: StateArrayItem? // Focus state for each text field
    
    //Sample array
    @State private var items: [StateArrayItem] = [
        StateArrayItem(id: 0, value: 40),
        StateArrayItem(id: 1, value: 68),
        StateArrayItem(id: 2, value: 52),
        StateArrayItem(id: 3, value: 48),
        StateArrayItem(id: 4, value: 33),
        StateArrayItem(id: 5, value: 71),
        StateArrayItem(id: 6, value: 69),
        StateArrayItem(id: 7, value: 22),
        StateArrayItem(id: 8, value: 28),
        StateArrayItem(id: 9, value: 85)
    ]
    
    //Body
    var body: some View {
        
        Form {
            
            //Section
            Section("Change a value and hit Return or tap away"){
                
                //Array items binding loop
                ForEach($items){ $item in
                    HStack {
                        
                        //Actual value for current ID
                        Text("ID: \($item.id) - Value: \(item.value)")
                            .foregroundStyle(item == selectedItem ? .blue : .primary)
                        
                        Spacer()
                        
                        //Given the $item binding, the value will update as soon as a value is entered
                        TextField("ID: \($item.id) - Value is: \($item.value)", value: $item.value, format: .number)
                            .foregroundStyle(item == selectedItem ? .blue : .primary)
                            .multilineTextAlignment(.trailing)
                            .frame(width: 80)
                            .textFieldStyle(.roundedBorder)
                            .focused($focusedField, equals: item)
                            .onChange(of: focusedField) { oldValue, newValue in
                                selectedItem =  newValue != nil ? newValue : selectedItem
                            }
                            .onSubmit {
                                selectedItem = item
                            }
                    }
                    .frame(maxWidth: .infinity, alignment: .center)
                }
            }
            
            //Section
            Section("Update by ID"){
                HStack {
                    Picker("Pick an ID:", selection: $selectedItem){
                        Text("--").tag(nil as StateArrayItem?) // Tag for the placeholder
                        ForEach($items){ $item in
                                Text("\($item.id)").tag(item)
                        }
                    }
                    .tint(.blue)
                    .fixedSize()
                }
                    
                //Show input form and button if an id is selected in the picker (or another textfield focused)
                if let item = selectedItem {
                    HStack(spacing: 10) {
                        TextField("Enter new value", value: $pickerFieldValue, format: .number)
                            .textFieldStyle(.roundedBorder)
                            .focused($focusedField, equals: item)
                            .onSubmit{
                                if let value = pickerFieldValue {
                                    updateValue(of: item, value: value)
                                }
                            }
                            .foregroundStyle(pickerFieldValue == nil ? .blue : pickerFieldValue != nil && item.value != pickerFieldValue ? .red : .green)
                        
                        Spacer()
                        
                        //Actual value for ID selected in the picker
                        Text("ID: \(item.id) Value: \(item.value)")
                    }
                    
                    //Update button
                    Button("Update", action: {
                        //Guard condition for running the update function
                        guard let value = pickerFieldValue else {
                            print("No value to update with...")
                            return
                        }
                        updateValue(of: item, value: value)
                    })
                    .buttonStyle(.borderedProminent)
                    .disabled((pickerFieldValue == nil))
                }
            }
        }
    }
    
    //Function to update an item with a new value
    func updateValue(of item: StateArrayItem, value: Int) {
        
        //Find the item in the array, based on its index - since item itself is a let constant
        guard let index = items.firstIndex(of: item) else { return }
        
        //Update the value
        items[index].value = value
        
        //Update the selectedItem with the newly modified item
        selectedItem = items[index]
    }
}

#Preview {
    StateArrayContentView()
}

enter image description here

Sign up to request clarification or add additional context in comments.

Comments

1

Try this approach using .onAppear to populate the array of items, then to call your function to change the value of a chosen index.

The example code shows the array of structs is changed as desired and the UI is refreshed as expected, that is achieve your goal of ...when a button is pressed I want to change a value of one of them (and have the UI automatically update).

Note from the docs, State "When the value changes, SwiftUI updates the parts of the view hierarchy that depend on the value". Meaning, if the parts of the view do not depend on the changes, they will not be refreshed.

struct Item: Codable, Identifiable {
    var id: Int
    var value: Int
}

struct ContentView: View {
    @State private var items: [Item] = []
    
    var body: some View {
        NavigationStack {
            List {
                HStack {
                    Text("id")
                    Spacer()
                    Text("value")
                }
                ForEach (items) { item in
                    HStack {
                        Text("\(item.id)")
                        Spacer()
                        Text("\(item.value)")
                    }
                }
            }
            Button("test") {
                updateValueOfItem(id: 1, value: 100)
            }
        }
        .onAppear {
            // give the items some values
            items = [Item(id: 0, value: 0), Item(id: 1, value: 1), Item(id: 2, value: 2)]
        }
    }

    func updateValueOfItem(id: Int, value: Int) {
        // initially the array is empty, because you have `... var items: [Item] = []`
        // so filter will always be empty as well, hence added the .onAppear
  //      items.indices.filter { items[$0].id == id }.forEach { items[$0].value = value }
  //      print("array = \(items)")
    
       // alternatively
       if id < items.count {
           items[id].value = value
       }

    }
    
}

Comments

0

Item should be:

struct Item: Codable, Identifiable {
    let id = UUID()
    var value: Int
}

I.e. make it impossible to change the id by making it a let.

Make some initial items, eg

@State private var items: [Item] = [.init(value: 0)]

items[0].value = value was right.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.