Reputation: 7437
I am trying to convert the Landmarks app to SwiftData... why isn't this class conforming to codable/decodable? It won't compile but the messages are non-specific:
"Type 'Landmark' does not conform to protocol 'Decodable'" "Type 'Landmark' does not conform to protocol 'Encodable'"
and
In expansion of macro 'Model' here: "Cannot automatically synthesize 'Decodable' because 'any SwiftData.BackingData' does not conform to 'Decodable'"
import Foundation
import SwiftUI
import CoreLocation
import SwiftData
@Model
final class Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
var park: String
var state: String
var dezcription: String
var isFavorite: Bool
var isFeatured: Bool
var category: Category
private var coordinates: Coordinates
private var imageName: String
init(id:Int = Int.random(in: 2000...Int.max), name:String = "", park: String = "", state:String = "", dezcription: String = "", isFavorite: Bool = false, isFeatured:Bool = false, category: Category = Category.mountains, coordinates: Coordinates = Coordinates(latitude: 0, longitude: 0), imageName:String = "umbagog") {
self.id = id
self.name = name
self.park = park
self.state = state
self.dezcription = dezcription
self.isFavorite = isFavorite
self.isFeatured = isFeatured
self.category = category
self.coordinates = coordinates
self.imageName = imageName
}
enum Category: String, CaseIterable, Codable {
case lakes = "Lakes"
case rivers = "Rivers"
case mountains = "Mountains"
}
var image: Image {
Image(imageName)
}
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
PS - I know that the design of this (the random int id... the default location of 0,0) are bad... but that's not the point of the question.
I've tried:
Upvotes: 0
Views: 377
Reputation: 7437
Sorry, this is a case where I made a false assumption in my question — That this class needs to be codable.
The reason the class was marked Codable was because that was the way it was in the original Landmarks app and I didnt think to change it.
What I didn't realize is that a class marked with @Model does not require any explicit protocols, and in fact should not be marked codable unless one plans on doing some serious surgery as in Sweeper's answer.
If I simply remove the Codable protocol (and can also remove all other protocols safely), it works.
I enterpreted the error message as a Swiftdata error when it is just a “normal” error saying “this class is not codable [because of Swiftdata macro]”
So the other previous answers are appreciated and valid if anyone out there actually has a class they need to be a swift data model and codable, but in my case it was just a false assumption.
One could say that this renders the question invalid, but that is hindsight and I think this the answer to my question, and the information will be valuable to people with the same misunderstanding.
Upvotes: -2
Reputation: 271410
The reason this happens is that the synthesis of the Codable
implementation happens after the macros have expanded.
After the macros have expanded, all the properties you declared become computed properties, and the macros add an additional _$backingData
property, as well as an underscored-prefixed version of each property you declared, of type _SwiftDataNoType
(just an empty struct, acting as a placeholder), among other things.
Basically, @Model
turns this:
class Foo {
var foo: Int
var bar: String
}
into:
class Foo: PersisrentModel {
var foo: Int { get { ... } set { ... } }
var bar: String { get { ... } set { ... } }
private var _foo: _SwiftDataNoType
private var _bar: _SwiftDataNoType
private var _$backingData: any SwiftData.BackingData<Foo>
}
No wonder the Codable
synthesis fails!
I recommend checking out the actual code that it generates (the above is only a simplified version) with Xcode's "Inline Macro" and "Expand Macro" context menu items.
You can conform to Codable
manually, like this article shows. See also workingdog's answer.
Alternatively, try writing a macro that generates a Codable
implementation. Your macro would receive the same syntax tree as @Model
, so you will have access to the class' members before the macro expansion. You'd use it like this:
@Model
@CodableModel
final class Landmark: Hashable, Identifiable {
var id: Int
var name: String
...
}
Here is a rough sketch of such a CodableModel
:
@attached(member, names: named(init(from:)), named(encode(to:)), named(CodingKeys))
@attached(extension, conformances: Codable)
public macro CodableModel() = #externalMacro(module: "...", type: "CodableModel")
// ...
public struct CodableModel: MemberMacro, ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
var properties = [(type: TypeSyntax, name: TokenSyntax)]()
let members = declaration.memberBlock.members
for member in members {
guard let propertyDecl = member.decl.as(VariableDeclSyntax.self) else { continue }
guard propertyDecl.bindingSpecifier.text == "var" else { continue } // ignore lets
guard let binding = propertyDecl.bindings.first else { continue }
guard binding.accessorBlock == nil else { continue } // ignore computed properties
guard let type = binding.typeAnnotation?.type else { continue }
guard let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else { continue }
properties.append((type, name))
}
let codingKeys = try EnumDeclSyntax("enum CodingKeys: String, CodingKey") {
for (_, name) in properties {
"case \(name)"
}
}
let initialiser = try InitializerDeclSyntax("init(from decoder: Decoder) throws") {
"let container = try decoder.container(keyedBy: CodingKeys.self)"
for (type, name) in properties {
"\(name) = try container.decode(\(type).self, forKey: .\(name))"
// TODO: use decodeIfPresent if the type is optional
}
}
let encodeMethod = try FunctionDeclSyntax("func encode(to encoder: Encoder) throws") {
"var container = encoder.container(keyedBy: CodingKeys.self)"
for (_, name) in properties {
"try container.encode(\(name), forKey: .\(name))"
}
}
return [
DeclSyntax(codingKeys),
DeclSyntax(initialiser),
DeclSyntax(encodeMethod),
]
}
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
[try ExtensionDeclSyntax("extension \(type): Codable") {}]
}
}
Upvotes: 1
Reputation: 36304
Try this approach of using a specific public init(from decoder: Decoder)
and
public func encode(to encoder: Encoder)
to make your Landmark
Codable.
Note you cannot have var image: Image
, Image is a View and should not be part of your model.
Similarly, var locationCoordinate: CLLocationCoordinate2D {...}
is not Codable, use a function
instead, as shown in the example code.
@Model
final class Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
var park: String
var state: String
var dezcription: String
var isFavorite: Bool
var isFeatured: Bool
var category: Category
private var coordinates: Coordinates
private var imageName: String
enum CodingKeys: CodingKey {
case id, name, park, state, dezcription, isFavorite, isFeatured
case category, coordinates, imageName
}
init(id:Int = Int.random(in: 2000...Int.max), name:String = "", park: String = "", state:String = "", dezcription: String = "", isFavorite: Bool = false, isFeatured:Bool = false, category: Category = Category.mountains, coordinates: Coordinates = Coordinates(latitude: 0, longitude: 0), imageName:String = "umbagog") {
self.id = id
self.name = name
self.park = park
self.state = state
self.dezcription = dezcription
self.isFavorite = isFavorite
self.isFeatured = isFeatured
self.category = category
self.coordinates = coordinates
self.imageName = imageName
}
enum Category: String, CaseIterable, Codable {
case lakes = "Lakes"
case rivers = "Rivers"
case mountains = "Mountains"
}
// var image: Image {
// Image(imageName) // <--- should NOT be here, Image is a View
// }
// not Codable, use a function
// var locationCoordinate: CLLocationCoordinate2D {
// CLLocationCoordinate2D(
// latitude: coordinates.latitude,
// longitude: coordinates.longitude)
// }
func locationCoordinate() -> CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
park = try container.decode(String.self, forKey: .park)
state = try container.decode(String.self, forKey: .state)
dezcription = try container.decode(String.self, forKey: .dezcription)
isFavorite = try container.decode(Bool.self, forKey: .isFavorite)
isFeatured = try container.decode(Bool.self, forKey: .isFeatured)
category = try container.decode(Category.self, forKey: .category)
coordinates = try container.decode(Coordinates.self, forKey: .coordinates)
imageName = try container.decode(String.self, forKey: .imageName)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(park, forKey: .park)
try container.encode(state, forKey: .state)
try container.encode(dezcription, forKey: .dezcription)
try container.encode(isFavorite, forKey: .isFavorite)
try container.encode(isFeatured, forKey: .isFeatured)
try container.encode(category, forKey: .category)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(imageName, forKey: .imageName)
}
}
Upvotes: 0