iosswiftswiftuicodablemutating-function

SwiftUI - Editing a value of an item in a Codable Array Cannot use mutating member on immutable value


I have a built up List with using an array of Codable type called Package. At some point, I need to update the isFavorite value of an Package item when user taps favorite button in the list. However I am getting the error of Cannot use mutating member on immutable value: 'package' is a 'let' constant. First I tried to directly change the value in the action. It did not work, so the point I got so far is below.

Obviously the code is longer than that but I just wanted make it simple and deleted irrelevant parts.

SwiftUIView

struct AView: View {

    @ObservedObject var store: JSONController

    var body: some View {
        NavigationView {
            List(self.store.packages) { (package) in
                return self.row(package)
            }
        }
    }

    private func row(_ package: Package) -> some View {
        HStack{
            Text(package.name)
            Spacer()
            Button(action: {
                package.changeFavoriteState()
            }) {
                Image(systemName: package.isFavorite ? "star.fill" : "star")
            }.buttonStyle(PlainButtonStyle())
        }
    }
}

Package.struct

struct Package: Codable, Identifiable {

    var id: UUID
    var isFavorite: Bool

    mutating func changeFavoriteState() {
        self.isFavorite.toggle()
    }
}

PackageList.struct

struct PackageList: Codable {

    var packages: [Package]

    private enum CodingKeys: CodingKey {
        case packages
    }

    init(){
        self.packages = [Package]()
    }

    init(from decoder: Decoder) throws {
        do {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.packages = try container.decode([Package].self, forKey: .packages)
        }catch let error {
            print(error.localizedDescription+" Initializing packages with an empty array.")
            self.packages = [Package]()
        }
    }
}

JSONController.class

class JSONController: ObservableObject {

    @Published var packages: [Package] = [Package]()

    init(){
        self.readJSONFile(from: "packageList")
    }

    func readJSONFile(from url: String) {
        //stuff
    }

}

And finally the error I got,

error


Solution

  • it is simple, you need access the value, not its copy, provided by List

    import SwiftUI
    
    struct Package {
        var flag: Bool
    }
    
    struct ContentView: View {
        @State var arr = [Package(flag: true), Package(flag: false), Package(flag: true)]
        var body: some View {
            List(arr.indices, id:\.self) { (index) in
                Text(self.arr[index].flag.description).onTapGesture {
                    self.arr[index].flag.toggle()
                }
            }
        }
    }
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    enter image description here

    or using the ObservableObject

    import SwiftUI
    
    struct Package: Identifiable {
        let id = UUID()
        var flag: Bool
    }
    class Model: ObservableObject {
        @Published var arr = [Package(flag: true), Package(flag: false), Package(flag: true)]
    }
    
    struct ContentView: View {
        @ObservedObject var model = Model()
        var body: some View {
            List(model.arr.indices) { (idx) in
                Text(self.model.arr[idx].flag.description)
                    .onTapGesture {
                        self.model.arr[idx].flag.toggle()
                }
            }
        }
    }
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    which is generally the same ... but in more complex scenarios preferable

    UPDATE based on request You have to redefine row function

    import SwiftUI
    
    struct Package: Identifiable {
        let id = UUID()
        var flag: Bool
    }
    class Model: ObservableObject {
        @Published var arr = [Package(flag: true), Package(flag: false), Package(flag: true)]
    }
    
    struct ContentView: View {
        @ObservedObject var model = Model()
        var body: some View {
            List(model.arr.indices) { (idx) in
                self.row(flag: self.$model.arr[idx].flag)
            }
        }
    
        func row(flag: Binding<Bool>)-> some View {
            Text(flag.wrappedValue.description)
                .onTapGesture {
                    flag.wrappedValue.toggle()
            }
        }
    }
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    Or the second option

    import SwiftUI
    
    struct Package: Identifiable {
        let id = UUID()
        var flag: Bool
    }
    class Model: ObservableObject {
        @Published var arr = [Package(flag: true), Package(flag: false), Package(flag: true)]
    }
    
    struct ContentView: View {
        @ObservedObject var model = Model()
        var body: some View {
            List(model.arr.indices) { (idx) in
                self.row(idx: idx)
            }
        }
    
        func row(idx: Int)-> some View {
            Text(model.arr[idx].flag.description)
                .onTapGesture {
                    self.model.arr[idx].flag.toggle()
            }
        }
    }
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    UPDATE with conditional sorting

    import SwiftUI
    
    struct Package: Identifiable {
        let id: Int
        var flag: Bool
    }
    class Model: ObservableObject {
        @Published var arr = [Package(id: 1, flag: true), Package(id: 3, flag: false), Package(id: 2, flag: true)]
        @Published var ascending = false
    
        var sorted: [Package] {
            arr.sorted { (p0, p1) -> Bool in
                let sort = (p0.id < p1.id)
                return ascending ? !sort : sort
            }
        }
    }
    
    struct ContentView: View {
        @ObservedObject var model = Model()
    
        var body: some View {
    
            VStack {
                Toggle(isOn: $model.ascending) {
                    Text("ascending")
                }.padding(.horizontal)
    
                List(model.sorted.indices, id: \.self) { (idx) in
                    self.row(idx: idx)
                }
            }
        }
    
        func row(idx: Int)-> some View {
            Text(verbatim: model.sorted[idx].flag.description + ": " + model.sorted[idx].id.description)
                .onTapGesture {
                    if let i = self.model.arr.firstIndex (where: { (p) -> Bool in
                        self.model.sorted[idx].id == p.id
                    }) {
                        self.model.arr[i].flag.toggle()
                    }
            }
        }
    }
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }