TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

Mutating value of object in array of bindings

Forums > SwiftUI

Can somone explain why the following code doesn't succeed at changing the value of the title. Just click on "Add" first and then "Change value". I know there's other ways to do this but I just want to understand why it's not working. if you p theUnwrappedToDo.title at line 26, you will see the value has changed. If you click on "Change Value" again you will see that the value is back to its initial state.

import SwiftUI

struct ContentView: View {
    @State var items: [TodoItem] = [TodoItem]()

    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach($items) { $item in
                        TextField("Title", text: $item.title)
                    }
                }
                Button("Add") {
                    items.append(TodoItem(id: UUID(), title: "Test"))

                }
                Button("Change value") {

                    let theToDo = items.first { todo in
                        todo.title == "Test"
                    }

                    if let theUnwrappedToDo = theToDo {
                        theUnwrappedToDo.title = "Super test"
                    }
                }
            }
            .navigationTitle("Todo list")
        }
    }
}

class TodoItem: Identifiable {
    internal init(id: UUID, title: String) {
        self.id = id
        self.title = title
    }

    let id: UUID
    @Published var title: String

}

2      

In order to make your class to emit changes you have to make it @Observable

First

import Observation

Second make the class Observable and you can remove @Published as it will be in any case publishing changes

@Observable
class TodoItem: Identifiable {
    internal init(id: UUID, title: String) {
        self.id = id
        self.title = title
    }

    let id: UUID
    var title: String

}

Now, everything works as, as soon as there is a change the object will publish the changes to view.

2      

Now what if it was a struct instead of a class?

struct TodoItem: Identifiable {

    internal init(id: UUID, title: String) {
        self.id = id
        self.title = title
    }

    let id: UUID
    var title: String
}

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Here you go:

struct TodoItem: Identifiable {
    let id = UUID()
    var title = ""
}

struct ContentView: View {
    @State var items = [TodoItem]()

    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach($items) { $item in
                        TextField("Title", text: $item.title)
                    }
                }
                Button("Add") {
                    items.append(TodoItem(title: "Test"))

                }
                Button("Change value") {
                    if let index = items.firstIndex(where: {$0.title == "Test"}) {
                        items[index].title = "Super Test"
                    }
                }
            }
            .navigationTitle("Todo list")
        }
    }
}

2      

Here is the way to work with both. In the below example you can separate the model from the view.

struct TodoItem: Identifiable {
    let id = UUID()
    var title = ""
}

// Also you can change to @Observable and remove @Published. (NB do not forget to import Observation for that)
// ObservableObject protocol was used prior introduction of @Observable Macros

class AppData: ObservableObject {
    @Published var items = [TodoItem]()

    func addTestTitle() {
        items.append(TodoItem(title: "Test"))
    }

    func changeToSuperTest() {
        if let index = items.firstIndex(where: {$0.title == "Test"}) {
            items[index].title = "Super Test"
        }
    }
}

struct ContentView: View {
    // In case you use @Observable Macros you will need to use @State instead of @StateObject
    @StateObject var appData = AppData()

    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach($appData.items) { $item in
                        TextField("Title", text: $item.title)
                    }
                }
                Button("Add") {
                    appData.addTestTitle()

                }
                Button("Change value") {
                    appData.changeToSuperTest()
                }
            }
            .navigationTitle("Todo list")
        }
    }
}

2      

Thanks for that. And that's precisely what I'd like to understand. If using a Struct, why does

if let index = items.firstIndex(where: {$0.title == "Test"}) {
                        items[index].title = "Super Test"
                    }

Work and not

 let theToDo = items.first { todo in
                        todo.title == "Test"
                    }
                        theToDo?.title = "Super test"

2      

For that you need to tell the difference between value types and reference types. Structs are value types and Classes are reference types. So basically when you create a struct and then assign it to a different variable you copy item and create kind of duplicate. In classes it works differently and you create a reference aka pointer to the same object in memory, so when you make updates to the class item you change it from different places. In structs you create different object and if you make updates you change updates to different objects not the same one.

Hope from the below code and comments it becomes clear what is going on if you do your way and why there is no update. So simply using the code offered by me, you avoid several lines of unnecessary code and get the same result.

struct TodoItem: Identifiable {
    let id = UUID()
    var title = ""
}

struct ContentView: View {
    @State var items = [TodoItem]()

    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach($items) { $item in
                        TextField("Title", text: $item.title)
                    }
                }
                Button("Add") {
                    items.append(TodoItem(title: "Test"))
                    // Here we find the item we just created
                    var newItem = items.first(where: { $0.title == "Test"})
                    // Let's find out memory location for that item
                    withUnsafePointer(to: &newItem) { print("\($0)") }
                }
                Button("Change value") {

                    var theToDo = items.first { todo in
                        todo.title == "Test"
                    }

                    // We can see from that print that location for that item is different
                    // So meaning the item differs from above
                    withUnsafePointer(to: &theToDo) { print("\($0)") }

                    if var theUnwrappedToDo = theToDo {
                        // We chagne the value of "different" TodoItem
                        // And we are not replacing the "original" one that we created in our array
                        theUnwrappedToDo.title = "Super test"

                        // HOW TO MAKE IT WORK:
                        // But if we add that part to your code
                        // Then we take that updated "version" of TodoItem and replace it in the array
                        // So making view to update and display the correct value

                        if let index = items.firstIndex(where: { $0.title == "Test"}) {
                            items[index] = theUnwrappedToDo
                        }

                    }
                }
            }
            .navigationTitle("Todo list")
        }
    }
}

2      

Haaaaa. Yes that should have ticked in my head.... of course. However since it is possible to mutate the property of the struct without specifically marking it with mutating func ( items[index].title = "Super Test"). Is it because of the @State var items = [TodoItem]() wrapper?

2      

View structures is diffent large topic, but shortly, yes @State works in such a way that you don't need to use mutating functions. So in your case your code works that way:

  1. You create "Test" TodoItem and append to items array. Since there is a change - state updates the view.
  2. You "copied" item "Test" to be "Super Test" TodoItem
  3. You find the index of "Test" in the items array and replace it with "Super Test", so there is a change in the array and state changes thus view udpates.

In your initial code with struct point 3 was never reached so the view was the same.

2      

Thanks heeps. Even with years of experience in ObjC and Swift, SwiftUI manages to make me wonder. There's so much behind the scene...

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.