In a earlier put up, I defined how one can make your NSManagedObject
subclasses codable. This was a considerably tedious course of that entails a bunch of handbook work. Particularly as a result of essentially the most handy means I’ve discovered wasn’t all that handy. It is simple to neglect to set your managed object context in your decoder’s person information dictionary which might lead to failed saves in Core Knowledge.
With SwiftData it is a lot simpler to outline mannequin objects so it is sensible to try making SwiftData fashions Codable to see if it is higher than Core Knowledge. In the end, SwiftData is a wrapper round Core Knowledge which signifies that the @Mannequin
macro will sooner or later generate managed objects, an object mannequin, and extra. On this put up, we’ll see if the @Mannequin
macro will even make it simpler to make use of Codable
with mannequin objects.
Tip: in case you’re not too accustomed to
Codable
or customized encoding and decoding of fashions, take a look at my put up collection on theCodable
protocol proper right here.
Defining a easy mannequin
On this put up I want to begin us off with a easy mannequin that is sufficiently small to not get complicated whereas nonetheless being consultant for a mannequin that you just may outline in the actual world. In my Sensible Core Knowledge ebook I make a variety of use of a Film
object that I take advantage of to symbolize a mannequin that I’d load from The Film Database. For comfort, let’s simply go forward and use the a simplified model of that:
@Mannequin class Film {
let originalTitle: String
let releaseDate: Date
init(originalTitle: String, releaseDate: Date) {
self.originalTitle = originalTitle
self.releaseDate = releaseDate
}
}
The mannequin above is easy sufficient, it has solely two properties and for instance the fundamentals of utilizing Codable with SwiftData we actually do not want something greater than that. So let’s transfer on and add Codable
to our mannequin subsequent.
Marking a SwiftData mannequin as Codable
The simplest method to make any Swift class or struct Codable
is to ensure the entire object’s properties are Codable
and having the compiler generate any and all boilerplate for us. Since each String
and Date
are Codable
and people are the 2 properties on our mannequin, let’s examine what occurs after we make our SwiftData mannequin Codable
:
// Kind 'Film' doesn't conform to protocol 'Decodable'
// Kind 'Film' doesn't conform to protocol 'Encodable'
@Mannequin class Film: Codable {
let originalTitle: String
let releaseDate: Date
init(originalTitle: String, releaseDate: Date) {
self.originalTitle = originalTitle
self.releaseDate = releaseDate
}
}
The compiler is telling us that our mannequin is not Codable
. Nevertheless, if we take away the @Mannequin
macro from our code we’re sure that our mannequin is Codable
as a result of our code does compiler with out the @Mannequin
macro.
So what’s taking place right here?
A macro in Swift expands and enriches our code by producing boilerplate or different code for us. We are able to proper click on on the @Mannequin
macro and select increase macro to see what the @Mannequin
macro expands our code into. You do not have to totally perceive or grasp all the physique of code under. The purpose of displaying it’s to indicate you that the @Mannequin
macro provides a variety of code, together with properties that do not conform to Codable
.
@Mannequin class Film: Codable {
@_PersistedProperty
let originalTitle: String
@_PersistedProperty
let releaseDate: Date
init(originalTitle: String, releaseDate: Date) {
self.originalTitle = originalTitle
self.releaseDate = releaseDate
}
@Transient
non-public var _$backingData: any SwiftData.BackingData<Film> = Film.createBackingData()
public var persistentBackingData: any SwiftData.BackingData<Film> {
get {
_$backingData
}
set {
_$backingData = newValue
}
}
static func schemaMetadata() -> [(String, AnyKeyPath, Any?, Any?)] {
return [
("originalTitle", Movie.originalTitle, nil, nil),
("releaseDate", Movie.releaseDate, nil, nil)
]
}
required init(backingData: any SwiftData.BackingData<Film>) {
self.persistentBackingData = backingData
}
@Transient
non-public let _$observationRegistrar = Remark.ObservationRegistrar()
}
extension Film: SwiftData.PersistentModel {
}
extension Film: Remark.Observable {
}
If we apply Codable
to our SwiftData mannequin, the protocol is not utilized to the small mannequin we have outlined. As an alternative, it is utilized to the totally expanded macro. Which means we’ve got a number of properties that do not conform to Codable
which makes it inconceivable for the compiler to (on the time of penning this) accurately infer what it’s that we need to do.
We are able to repair this by writing our personal encoding and decoding logic for our mannequin.
Writing your encoding and decoding logic
For an entire overview of writing customized encoding and decoding logic to your fashions, take a look at this put up.
Let’s begin off by defining the CodingKeys
enum that we’ll use for each our encoding and decoding logic:
@Mannequin class Film: Codable {
enum CodingKeys: CodingKey {
case originalTitle, releaseDate
}
// ...
}
These coding keys instantly observe the property names for our mannequin. We’ve to outline them as a result of we’re defining customized encoding and decoding logic.
The decoding init
can look as follows:
required init(from decoder: Decoder) throws {
let container = strive decoder.container(keyedBy: CodingKeys.self)
self.originalTitle = strive container.decode(String.self, forKey: .originalTitle)
self.releaseDate = strive container.decode(Date.self, forKey: .releaseDate)
}
This initializer is fairly easy. We seize a container from the decoder, after which we ask the container to decode the properties we’re concerned with utilizing our coding keys.
The encoding logic would look as follows:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
strive container.encode(originalTitle, forKey: .originalTitle)
strive container.encode(releaseDate, forKey: .releaseDate)
}
With this initializer and encode(to:)
perform in place, our mannequin is now totally Codable
. Observe that in case you’re solely grabbing knowledge from the community and which to decode that knowledge into SwiftData fashions you may conform to Decodable
as an alternative of Codable
so as to skip having to put in writing the encode(to:)
methodology.
Let’s have a look at how we will truly use our mannequin subsequent.
Decoding JSON right into a SwiftData mannequin
For essentially the most half, decoding your JSON knowledge right into a SwiftData mannequin might be comparatively striaghtforward. The important thing factor to bear in mind is that it is advisable register your whole decoded objects in your mannequin context after decoding them. This is an instance of how to do that:
let url = URL(string: "https://path.to.knowledge")!
let (knowledge, _) = strive await URLSession.shared.knowledge(from: url)
// that is the precise decoding
let motion pictures = strive! JSONDecoder().decode([Movie].self, from: knowledge)
// do not forget to register the decoded objects
for film in motion pictures {
context.insert(film)
}
Making our mannequin Codable
and dealing with it was easy sufficient. To wrap issues up, I might prefer to discover how this strategy works with relationships.
Including relationships to our mannequin
First, let’s replace our mannequin object to have a relationship:
@Mannequin class Film: Codable {
enum CodingKeys: CodingKey {
case originalTitle, releaseDate, solid
}
let originalTitle: String
let releaseDate: Date
@Relationship([], deleteRule: .cascade)
var solid: [Actor]
init(originalTitle: String, releaseDate: Date, solid: [Actor]) {
self.originalTitle = originalTitle
self.releaseDate = releaseDate
self.solid = solid
}
required init(from decoder: Decoder) throws {
let container = strive decoder.container(keyedBy: CodingKeys.self)
self.originalTitle = strive container.decode(String.self, forKey: .originalTitle)
self.releaseDate = strive container.decode(Date.self, forKey: .releaseDate)
self.solid = strive container.decode([Actor].self, forKey: .solid)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
strive container.encode(originalTitle, forKey: .originalTitle)
strive container.encode(releaseDate, forKey: .releaseDate)
strive container.encode(solid, forKey: .solid)
}
}
The Film
object right here has gained a brand new property solid
which is annotated with SwiftData’s @Relationship
macro. Observe that the decode and encode logic does not get fancier than it must be. We simply decode and encode our solid
property like we might some other property.
Let us take a look at the definition of our Actor
mannequin subsequent:
@Mannequin class Actor: Codable {
enum CodingKeys: CodingKey {
case identify
}
let identify: String
@Relationship([], deleteRule: .nullify)
let motion pictures: [Movie]
init(identify: String, motion pictures: [Movie]) {
self.identify = identify
self.motion pictures = motion pictures
}
required init(from decoder: Decoder) throws {
let container = strive decoder.container(keyedBy: CodingKeys.self)
self.identify = strive container.decode(String.self, forKey: .identify)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
strive container.encode(identify, forKey: .identify)
}
}
Our Actor
defines a relationship again to our Film
mannequin however we do not account for this in our encode and decode logic. The info we’re loading from an exterior supply would infinitely recurse from actor to film and again if actors would additionally maintain lists of their motion pictures within the knowledge we’re decoding. As a result of the supply knowledge does not comprise the inverse that we have outlined on our mannequin, we do not decode it. SwiftData will guarantee that our motion pictures
property is populated as a result of we have outlined this property utilizing @Relationship
.
When decoding our full API response, we needn’t replace the utilization code from earlier than. It appears to be like like we do not have to explicitly insert our Actor
cases into our mannequin context as a consequence of SwiftData’s dealing with of relationships which is sort of good.
With the code as it’s on this put up, we will encode and decode our SwiftData mannequin objects. No magic wanted!
In Abstract
All in all I’ve to say that I am a bit of unhappy that we did not get Codable
help for SwiftData objects without spending a dime. It is good that it is simpler to make SwiftData fashions Codable
than it’s to make an NSManagedObject
conform to Codable
nevertheless it’s not too far off. We nonetheless need to guarantee that we affiliate our decoded mannequin with a context. It is just a bit bit simpler to do that in SwiftData than it’s in Core Knowledge.
If in case you have a special strategy to make your SwiftData fashions Codable
, or when you’ve got questions on this put up be at liberty to attain out!