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
}
}
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!
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.
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.