Search code examples
swiftuibindingobservable

Adding @Observable to class causes Immutable property will no be decoded… warning in SwiftUI


I have a very simple app. It loads programmers data from a JSON file, decodes it and shows programmers name, address and city in a Text view.

struct DecodeJson: View {   
    @State var decodedText: String = ""
    @State var encodedText: String = ""
    @State var programmers = Programmers()
    
    var body: some View {
        VStack {
            Button("Load JSON") {
                decodedText = ""
                programmers = load("programmers.json")
                for programmer in programmers.items {
                    decodedText.append("\(programmer.name), \(programmer.address.street), \(programmer.address.city)\n")
                }
            }
            Text(decodedText)
       }
    }
}

class Programmers: Codable, Identifiable {
    var items: [Programmer] = [
        Programmer(id: 1, name: "Programmer1", address: Address(street: "Street1", city: "City1"), changed: false),
        Programmer(id: 2, name: "Programmer2", address: Address(street: "Street2", city: "City2"), changed: false),
    ]
}

class Programmer: Codable, Identifiable {
    var id: Int
    var name: String
    var address: Address
    var changed: Bool
    
    init(id: Int, name: String, address: Address, changed: Bool) {
        self.id = id
        self.name = name
        self.address = address
        self.changed = changed
    }
}

class Address: Codable, Identifiable {
    var street: String
    var city: String
    
    init(street: String, city: String) {
        self.street = street
        self.city = city
    }
}

This works well, but instead of showing in programmers data in a simple text box I need a programmer editor view because I want to edit a programmer and save the changes back to the JSON file. So I need to two-way binding between programmer’s name and address to the editor view. I should wrap the 3 classes with @Observale wrapper and use @Bindable in the editor view.

The problem is, when I add the @Observale wrapper - say - to Address class and build the app then I get a foggy warning: Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten.

// Added @Observable
@Observable class Address: Codable, Identifiable {
    var street: String
    var city: String
    
    init(street: String, city: String) {
        self.street = street
        self.city = city
    }
}
  1. I do not have immutable property. Both the city and the street are mutable var.
  2. None of the variables are initialized. Very good error message!

If I run the app with @Observale wrapped classes then it does not load the JSON file, even it reports errors in JSON file which is loaded fine without @Observale wrapper...

Is somebody who understands this? Thanks for the assistance!

I rewrote the app, at least 10 times, I tried Gooling the error message at Apple and even at stackoverflow but I could not find any solution for this. I need help. Thanks guys!


Solution

  • The synthesis of Codable conformance happens after macro expansion, so the error message is actually talking about code that is generated by the @Observable macro, which is why it is so confusing. Namely, it is talking about the observation registrar:

    @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()
    

    @Observable also turns all your stored properties into computed properties, and generates new stored properties with an underscore prefix. This is why the decoding fails, as there are no underscore-prefixed keys in the JSON.

    I would just implement init(from:) and encode(to:) by hand. Xcode 15 actually generates these for you as part of the auto-complete. Before adding @Observable, start typing init and encode in the class, and choose the options that have { ... } at the end.

    enter image description here

    enter image description here

    Full code for all 3 classes:

    @Observable
    class Programmers: Codable, Identifiable {
        var items: [Programmer] = [
            Programmer(id: 1, name: "Programmer1", address: Address(street: "Street1", city: "City1"), changed: false),
            Programmer(id: 2, name: "Programmer2", address: Address(street: "Street2", city: "City2"), changed: false),
        ]
        
        required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.items = try container.decode([Programmer].self, forKey: .items)
        }
        
        enum CodingKeys: CodingKey {
            case items
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(self.items, forKey: .items)
        }
    }
    
    @Observable
    class Programmer: Codable, Identifiable {
        var id: Int
        var name: String
        var address: Address
        var changed: Bool
        
        init(id: Int, name: String, address: Address, changed: Bool) {
            self.id = id
            self.name = name
            self.address = address
            self.changed = changed
        }
        
        required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.id = try container.decode(Int.self, forKey: .id)
            self.name = try container.decode(String.self, forKey: .name)
            self.address = try container.decode(Address.self, forKey: .address)
            self.changed = try container.decode(Bool.self, forKey: .changed)
        }
        
        enum CodingKeys: CodingKey {
            case id
            case name
            case address
            case changed
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(self.id, forKey: .id)
            try container.encode(self.name, forKey: .name)
            try container.encode(self.address, forKey: .address)
            try container.encode(self.changed, forKey: .changed)
        }
    }
    
    @Observable
    class Address: Codable, Identifiable {
        var street: String
        var city: String
        
        init(street: String, city: String) {
            self.street = street
            self.city = city
        }
        
        required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.street = try container.decode(String.self, forKey: .street)
            self.city = try container.decode(String.self, forKey: .city)
        }
        
        enum CodingKeys: CodingKey {
            case street
            case city
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(self.street, forKey: .street)
            try container.encode(self.city, forKey: .city)
        }
    }
    

    That said, I don't see why you need them to be classes. These classes look like they represent simple data for your app, and can totally be structs. You should try to use structs as much as possible in SwiftUI. SwiftUI can detect changes in structs without anything extra like @Observable. The only places where you need a class is when you need it to stay in memory persistently, e.g. delegates for other classes, SwiftData models etc.