Nothing Special   »   [go: up one dir, main page]

Codable With Core Data and NSManagedObject

Download as pdf or txt
Download as pdf or txt
You are on page 1of 11

Using Codable with Core Data

and NSManagedObject
Published on: August 3, 2020
CODABLE CORE DATA SWIFT
If you've ever wanted to decode a bunch of JSON data into
NSManagedObject instances you've probably noticed that this isn't a
straightforward exercise. With plain structs, you can conform your
struct to Codable and you convert the struct from and to JSON data
automatically.
<img src="https://www.donnywals.com/wp-content/uploads/
RevenueCatLogo.png">
In-app purchases made easy. RevenueCat provides everything you need to
implement, manage, and analyze in-app purchases without managing servers
or writing backend code.

Try RevenueCat now

This sponsored message helps keep the content on this site free. Please check
out this sponsor as it directly supports me and this site.

For an NSManagedObject subclass it's not that easy.

If your Core Data data model is con gured to automatically


generate your entity class de nitions for you (which is the default),
you may have tried to write the following code to conform your
managed object to Decodable:

extension MyManagedObject: Decodable { }


If you do this, the compiler will tell you that it can't synthesize an
implementation for init(from:) for a class that's de ned in a different
le. Xcode will offer you some suggestions like adding an initializer,
marking it as convenience and eventually the errors will point you
towards making your init required too, resulting in something like the
following:

extension MyManagedObject: Decodable {


required convenience public init(from decoder:
Decoder) throws {
}
}
Once you've written this you'll nd that Xcode still isn't happy and
that it presents you with the following error:

'required' initializer must be declared directly in class


'MyManagedObject' (not in an extension)

In this week's post, you will learn how you can manually de ne your
managed object subclass and add support for Swift's JSON
fi
fi
fi
fi
fi
fi
decoding and encoding features by conforming your managed
object to Decodable and Encodable. First, I'll explain how you can
tweak automatic class generation and de ne your managed object
subclasses manually while still generating the de nition for all of
your entity's properties.

After that, I'll show you how to conform your managed object to
Decodable, and lastly, we'll add conformance for Encodable as well to
make your managed object conform to the Codable protocol (which
is a combined protocol of Decodable and Encodable).

Tweaking your entity's code generation


Since we need to de ne our managed object subclass ourselves to
add support for Codable, you need to make some changes to how
Xcode generates code for you.

Open your xcdatamodeld le and select the entity that you want to
manually de ne the managed object subclass for. In the sidebar on
the right, activate the Data model inspector and set the Codegen
dropdown to Category/Extension. Make sure that you set Module to
Current product module and that Name is set to the name of the
managed object subclass that you will de ne. Usually, this class
name mirrors the name of your entity (but it doesn't have to).

<img src="https://
www.donnywals.com/wp-content/uploads/Screen-Shot-2020-08-03-
at-10.09.16.png" alt="" />
fi
fi
fi
fi
fi
fi
After setting up your data model, you can de ne your subclasses.
Since Xcode will generate an extension that contains all of the
managed properties for your entity, you only have to de ne the
classes that Xcode should extend:

class TodoItem: NSManagedObject {


}

class TodoCompletion: NSManagedObject {


}
Once you've de ned your managed object subclasses, Xcode
generates extensions for these classes that contain all of your
managed properties while giving you the ability to add the required
initializers for the Decodable and Encodable protocols.

Let's add conformance for Decodable rst.

Conforming an NSManagedObject to
Decodable
The Decodable protocol is used to convert JSON data into Swift
objects. When your objects are relatively simple and closely mirror
the structure of your JSON, you can conform the object to Decodable
and the Swift compiler generates all the required decoding code for
you.

Unfortunately, Swift can't generate this code for you when you want
to make your managed object conform to Decodable.

Because Swift can't generate the required code, we need to de ne


the init(from:) initializer ourselves. We also need to de ne the
CodingKeys object that de nes the JSON keys that we want to use
when decoding JSON data. Adding the initializer and CodingKeys for
the objects from the previous section looks as follows:

class TodoCompletion: NSManagedObject, Decodable {


enum CodingKeys: CodingKey {
case completionDate
fi
fi
fi
fi
fi
fi
fi
}

required convenience init(from decoder: Decoder)


throws {
}
}

class TodoItem: NSManagedObject, Decodable {


enum CodingKeys: CodingKey {
case id, label, completions
}

required convenience init(from decoder: Decoder)


throws {
}
}
Before I get to the decoding part, we need to talk about managed
objects a little bit more.

Managed objects are always associated with a managed object


context. When you want to create an instance of a managed object
you must pass a managed object context to the initializer.

When you're initializing your managed object with init(from:) you


can't pass the managed object context along to the initializer
directly. And since Xcode will complain if you don't call self.init from
within your convenience initializer, we need a way to make a
managed object context available within init(from:) so we can
properly initialize the managed object.

This can be achieved through JSONDecoder's userInfo dictionary. I'll


show you how to do this rst, and then I'll show you what this
means for the initializer of TodoItem from the code snippet I just
showed you. After that, I will show you what TodoCompletion ends up
looking like.
fi
Since all keys in JSONDecoder's userInfo must be of type
CodingUserInfoKey we need to extend CodingUserInfoKey rst to
create a managed object context key:

extension CodingUserInfoKey {
static let managedObjectContext =
CodingUserInfoKey(rawValue:
"managedObjectContext")!
}
We can use this key to set and get a managed object context from
the userInfo dictionary. Now let's create a JSONDecoder and set its
userInfo dictionary:

let decoder = JSONDecoder()


decoder.userInfo[CodingUserInfoKey.managedObjectCo
ntext] = myPersistentContainer.viewContext
When we use this instance of JSONDecoder to decode data, the
userInfo dictionary is available within the initializer of the object we're
decoding to. Let's see how this works:

enum DecoderConfigurationError: Error {


case missingManagedObjectContext
}

class TodoItem: NSManagedObject, Decodable {


enum CodingKeys: CodingKey {
case id, label, completions
}

required convenience init(from decoder: Decoder)


throws {
guard let context =
decoder.userInfo[CodingUserInfoKey.managedObjectCo
ntext] as? NSManagedObjectContext else {
fi
throw
DecoderConfigurationError.missingManagedObjectCont
ext
}

self.init(context: context)

let container = try decoder.container(keyedBy:


CodingKeys.self)
self.id = try container.decode(Int64.self,
forKey: .id)
self.label = try container.decode(String.self,
forKey: .label)
self.completions = try
container.decode(Set<TodoCompletion>.self, forKey:
.completions) as NSSet
}
}
In the initializer for TodoItem I try to extract the object at
CodingUserInfoKey.managedObjectContext from the Decoder's userInfo
dictionary and I try to cast it to an NSManagedObjectContext. If this
fails I throw an error that I've de ned myself because we can't
proceed without a managed object context.

After that, I call self.init(context: context) to initialize the TodoItem and


associate it with a managed object context.

The last step is to decode the object as you normally would by


grabbing a container that's keyed by CodingKeys.self and decoding
all relevant properties into the correct types.

Note that Core Data still uses Objective-C under the hood so you
might have to cast some Swift types to their Objective-C
counterparts like I had to with my Set<TodoCompletion>.

For completion, this is what the full class de nition for


TodoCompletion would look like:
fi
fi
class TodoCompletion: NSManagedObject, Decodable {
enum CodingKeys: CodingKey {
case completionDate
}

required convenience init(from decoder: Decoder)


throws {
guard let context =
decoder.userInfo[CodingUserInfoKey.managedObjectCo
ntext] as? NSManagedObjectContext else {
throw
DecoderConfigurationError.missingManagedObjectCont
ext
}

self.init(context: context)

let container = try decoder.container(keyedBy:


CodingKeys.self)
self.completionDate = try
container.decode(Date.self,
forKey: .completionDate)
}
}
This code shouldn't look surprising; it's basically the same as the
code for TodoItem. Note that the decoder that's used to decode the
TodoItem is also used to decode TodoCompletion which means that it
also has the managed object context in its userInfo dictionary.

If you want to test this code, you can use the following JSON as a
starting point:

[
{
"id": 0,
"label": "Item 0",
"completions": []
},
{
"id": 1,
"label": "Item 1",
"completions": [
{
"completionDate": 767645378
}
]
}
]
Unfortunately, it takes quite a bunch of code to make Decodable
work with managed objects, but the nal solution is something I'm
not too unhappy with. I like how easy it is to use once set up
properly.

Adding support for Encodable to an


NSManagedObject
While we had to do a bunch of custom work to add support for
Decodable to our managed objects, adding support for Encodable is
far less involved. All we need to do is de ne encode(to:) for the
objects that need Encodable support:

class TodoItem: NSManagedObject, Codable {


enum CodingKeys: CodingKey {
case id, label, completions
}

required convenience init(from decoder: Decoder)


throws {
// unchanged implementation
}
fi
fi
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy:
CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(label, forKey: .label)
try container.encode(completions as!
Set<TodoCompletion>, forKey: .completions)
}
}
Note that I had to convert completions (which is an NSSet) to a
Set<TodoCompletion> explicitly. The reason for this is that NSSet isn't
Encodable but Set<TodoCompletion> is.

For completion, this is what TodoCompletion looks like with Encodable


support:

class TodoCompletion: NSManagedObject, Codable {


enum CodingKeys: CodingKey {
case completionDate
}

required convenience init(from decoder: Decoder)


throws {
// unchanged implementation
}

func encode(to encoder: Encoder) throws {


var container = encoder.container(keyedBy:
CodingKeys.self)
try container.encode(completionDate,
forKey: .completionDate)
}
}
Note that there is nothing special that I had to do to conform my
managed object to Encodable compared to a normal manual
Encodable implementation.

In Summary
In this week's post, you learned how you can add support for
Codable to your managed objects by changing Xcode's default code
generation for Core Data entities, allowing you to write your own
class de nitions. You also saw how you can associate a managed
object context with a JSONDecoder through its userInfo dictionary,
allowing you to decode your managed objects directly from JSON
without any extra steps. To wrap up, you saw how to add Encodable
support, making your managed object conform to Codable rather
than just Decodable.
fi

You might also like