Search code examples
swiftxcodelistswiftuimutable

In a List. Left side of mutating operator isn't mutable: 'item' is a 'let' constant


Xcode 12. code and lifecycle are Swiftui.

I've looked at other peoples related questions and it seems like they just dump their entire projects code on here; it's a bit overwhelming to pick what I need from it. So, I've broken the issue down into the simplest example I can.

The goal is to have numberX iterate when I press the + button.

Thanks in advance!

struct InfoData: Identifiable {
    var id = UUID()
    var nameX: String
    var numberX: Int
}

class ContentX: ObservableObject {
    @Published var infoX = [
        InfoData(nameX: "Example", numberX: 1)
    ]
}

struct ContentView: View {
    @StateObject var contentX = ContentX()
    var body: some View {
        List(contentX.infoX) { item in
            HStack {
                Text("\(item.nameX)")
                Spacer()
                Button("+") {
                    item.numberX += 1 //Eror shows up here <<
                }
                Text("\(item.numberX)")
            }
        }
    }
}

Solution

  • In the syntax that you're using, item is an immutable value, as the error tells you. You can't mutate it, because it doesn't represent a true connection to the array it comes from -- it's just a temporary readable copy that is being used in the List iteration.

    If you can upgrade to Xcode 13, you have access to something called element binding syntax in List and ForEach that lets you do this:

    struct ContentView: View {
        @StateObject var contentX = ContentX()
        var body: some View {
            List($contentX.infoX) { $item in //<-- Here
                HStack {
                    Text("\(item.nameX)")
                    Spacer()
                    Button("+") {
                        item.numberX += 1
                    }
                    Text("\(item.numberX)")
                }
            }
        }
    }
    

    This gives you a Binding to item that is mutable, allowing you to change its value(s) and have them reflected in the original array.

    Prior to Xcode 13/Swift 5.5, you'd have to define your own way to alter an element in the array. This is one solution:

    class ContentX: ObservableObject {
        @Published var infoX = [
            InfoData(nameX: "Example", numberX: 1)
        ]
        
        func alterItem(item: InfoData) {
            self.infoX = self.infoX.map { $0.id == item.id ? item : $0 }
        }
    }
    
    struct ContentView: View {
        @StateObject var contentX = ContentX()
        var body: some View {
            List(contentX.infoX) { item in
                HStack {
                    Text("\(item.nameX)")
                    Spacer()
                    Button("+") {
                        var newItem = item
                        newItem.numberX += 1
                        contentX.alterItem(item: newItem)
                    }
                    Text("\(item.numberX)")
                }
            }
        }
    }
    

    Or, another option where a custom Binding is used:

    
    class ContentX: ObservableObject {
        @Published var infoX = [
            InfoData(nameX: "Example", numberX: 1)
        ]
        
        func bindingForItem(item: InfoData) -> Binding<InfoData> {
            .init {
                self.infoX.first { $0.id == item.id }!
            } set: { newValue in
                self.infoX = self.infoX.map { $0.id == item.id ? newValue : $0 }
            }
        }
    }
    
    struct ContentView: View {
        @StateObject var contentX = ContentX()
        var body: some View {
            List(contentX.infoX) { item in
                HStack {
                    Text("\(item.nameX)")
                    Spacer()
                    Button("+") {
                        contentX.bindingForItem(item: item).wrappedValue.numberX += 1
                    }
                    Text("\(item.numberX)")
                }
            }
        }
    }