Making your SwiftData fashions Codable – Donny Wals


In a earlier submit, I defined how one can make your NSManagedObject subclasses codable. This was a considerably tedious course of that includes a bunch of guide work. Particularly as a result of probably 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 consumer information dictionary which might lead to failed saves in Core Information.

With SwiftData it is a lot simpler to outline mannequin objects so it is smart to try making SwiftData fashions Codable to see if it is higher than Core Information. Finally, SwiftData is a wrapper round Core Information which signifies that the @Mannequin macro will sooner or later generate managed objects, an object mannequin, and extra. On this submit, we’ll see if the @Mannequin macro can even make it simpler to make use of Codable with mannequin objects.

If you happen to favor studying by video, try the video for this submit on YouTube:

Tip: should you’re not too accustomed to Codable or customized encoding and decoding of fashions, try my submit sequence on the Codable protocol proper right here.

Defining a easy mannequin

On this submit 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 would possibly outline in the actual world. In my Sensible Core Information e book I make a number of use of a Film object that I take advantage of to characterize 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 straightforward sufficient, it has solely two properties and for example 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 best technique to make any Swift class or struct Codable is to verify 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 once we make our SwiftData mannequin Codable:

// Sort 'Film' doesn't conform to protocol 'Decodable'
// Sort '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 occurring right here?

A macro in Swift expands and enriches our code by producing boilerplate or different code for us. We will proper click on on the @Mannequin macro and select develop macro to see what the @Mannequin macro expands our code into. You do not have to completely perceive or grasp your entire physique of code beneath. The purpose of exhibiting it’s to indicate you that the @Mannequin macro provides a number 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
  personal 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
  personal 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 a substitute, it is utilized to the absolutely expanded macro. Which means we’ve got a number of properties that do not conform to Codable which makes it unimaginable for the compiler to (on the time of scripting this) appropriately infer what it’s that we wish to do.

We will repair this by writing our personal encoding and decoding logic for our mannequin.

Writing your encoding and decoding logic

For a whole overview of writing customized encoding and decoding logic in your fashions, try this submit.

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 comply with the property names for our mannequin. We have now 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 = attempt decoder.container(keyedBy: CodingKeys.self)
  self.originalTitle = attempt container.decode(String.self, forKey: .originalTitle)
  self.releaseDate = attempt container.decode(Date.self, forKey: .releaseDate)
}

This initializer is fairly simple. We seize a container from the decoder, after which we ask the container to decode the properties we’re all for utilizing our coding keys.

The encoding logic would look as follows:

func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  attempt container.encode(originalTitle, forKey: .originalTitle)
  attempt container.encode(releaseDate, forKey: .releaseDate)
}

With this initializer and encode(to:) operate in place, our mannequin is now absolutely Codable. Notice that should 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 as a way to skip having to put in writing the encode(to:) technique.

Let’s have a look at how we will truly use our mannequin subsequent.

Decoding JSON right into a SwiftData mannequin

For probably 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 you should register all your 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, _) = attempt await URLSession.shared.knowledge(from: url)

// that is the precise decoding
let motion pictures = attempt! JSONDecoder().decode([Movie].self, from: knowledge)

// remember to register the decoded objects
for film in motion pictures {
  context.insert(film)
}

Making our mannequin Codable and dealing with it was simple 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, forged
  }

  let originalTitle: String
  let releaseDate: Date

  @Relationship([], deleteRule: .cascade)
  var forged: [Actor]

  init(originalTitle: String, releaseDate: Date, forged: [Actor]) {
    self.originalTitle = originalTitle
    self.releaseDate = releaseDate
    self.forged = forged
  }

  required init(from decoder: Decoder) throws {
    let container = attempt decoder.container(keyedBy: CodingKeys.self)
    self.originalTitle = attempt container.decode(String.self, forKey: .originalTitle)
    self.releaseDate = attempt container.decode(Date.self, forKey: .releaseDate)
    self.forged = attempt container.decode([Actor].self, forKey: .forged)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    attempt container.encode(originalTitle, forKey: .originalTitle)
    attempt container.encode(releaseDate, forKey: .releaseDate)
    attempt container.encode(forged, forKey: .forged)
  }
}

The Film object right here has gained a brand new property forged which is annotated with SwiftData’s @Relationship macro. Notice that the decode and encode logic does not get fancier than it must be. We simply decode and encode our forged 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 = attempt decoder.container(keyedBy: CodingKeys.self)
    self.identify = attempt container.decode(String.self, forKey: .identify)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    attempt 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 ensure 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 need not replace the utilization code from earlier than. It appears like we do not have to explicitly insert our Actor cases into our mannequin context resulting from SwiftData’s dealing with of relationships which is kind of good.

With the code as it’s on this submit, 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 unhappy that we did not get Codable assist for SwiftData objects without cost. It is good that it is simpler to make SwiftData fashions Codable than it’s to make an NSManagedObject conform to Codable but it surely’s not too far off. We nonetheless should ensure 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 Information.

When you have a unique strategy to make your SwiftData fashions Codable, or in case you have questions on this submit be at liberty to attain out!



Recent Articles

Related Stories

Leave A Reply

Please enter your comment!
Please enter your name here

Stay on op - Ge the daily news in your inbox