How to use swift 4 Codable in Core Data? - swift

Codable seems a very exciting feature. But I wonder how we can use it in Core Data? In particular, is it possible to directly encode/decode a JSON from/to a NSManagedObject?
I tried a very simple example:
and defined Foo myself:
import CoreData
#objc(Foo)
public class Foo: NSManagedObject, Codable {}
But when using it like this:
let json = """
{
"name": "foo",
"bars": [{
"name": "bar1",
}], [{
"name": "bar2"
}]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let foo = try! decoder.decode(Foo.self, from: json)
print(foo)
The compiler failed with this errror:
super.init isn't called on all paths before returning from initializer
and the target file was the file that defined Foo
I guess I probably did it wrong, since I didn't even pass a NSManagedObjectContext, but I have no idea where to stick it.
Does Core Data support Codable?

You can use the Codable interface with CoreData objects to encode and decode data, however it's not as automatic as when used with plain old swift objects. Here's how you can implement JSON Decoding directly with Core Data objects:
First, you make your object implement Codable. This interface must be defined on the object, and not in an extension. You can also define your Coding Keys in this class.
class MyManagedObject: NSManagedObject, Codable {
#NSManaged var property: String?
enum CodingKeys: String, CodingKey {
case property = "json_key"
}
}
Next, you can define the init method. This must also be defined in the class method because the init method is required by the Decodable protocol.
required convenience init(from decoder: Decoder) throws {
}
However, the proper initializer for use with managed objects is:
NSManagedObject.init(entity: NSEntityDescription, into context: NSManagedObjectContext)
So, the secret here is to use the userInfo dictionary to pass in the proper context object into the initializer. To do this, you'll need to extend the CodingUserInfoKey struct with a new key:
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")
}
Now, you can just as the decoder for the context:
required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }
guard let entity = NSEntityDescription.entity(forEntityName: "MyManagedObject", in: context) else { fatalError() }
self.init(entity: entity, in: context)
let container = decoder.container(keyedBy: CodingKeys.self)
self.property = container.decodeIfPresent(String.self, forKey: .property)
}
Now, when you set up the decoding for Managed Objects, you'll need to pass along the proper context object:
let data = //raw json data in Data object
let context = persistentContainer.newBackgroundContext()
let decoder = JSONDecoder()
decoder.userInfo[.context] = context
_ = try decoder.decode(MyManagedObject.self, from: data) //we'll get the value from another context using a fetch request later...
try context.save() //make sure to save your data once decoding is complete
To encode data, you'll need to do something similar using the encode protocol function.

CoreData is its own persistence framework and, per its thorough documentation, you must use its designated initializers and follow a rather specific path to creating and storing objects with it.
You can still use Codable with it in limited ways just as you can use NSCoding, however.
One way is to decode an object (or a struct) with either of these protocols and transfer its properties into a new NSManagedObject instance you've created per Core Data's docs.
Another way (which is very common) is to use one of the protocols only for a non-standard object you want to store in a managed object's properties. By "non-standard", I mean anything thst doesn't conform to Core Data's standard attribute types as specified in your model. For example, NSColor can't be stored directly as a Managed Object property since it's not one of the basic attribute types CD supports. Instead, you can use NSKeyedArchiver to serialize the color into an NSData instance and store it as a Data property in the Managed Object. Reverse this process with NSKeyedUnarchiver. That's simplistic and there is a much better way to do this with Core Data (see Transient Attributes) but it illustrates my point.
You could also conceivably adopt Encodable (one of the two protocols that compose Codable - can you guess the name of the other?) to convert a Managed Object instance directly to JSON for sharing but you'd have to specify coding keys and your own custom encode implementation since it won't be auto-synthesized by the compiler with custom coding keys. In this case you'd want to specify only the keys (properties) you want to be included.
Hope this helps.

Swift 4.2:
Following casademora's solution,
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError() }
should be
guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }.
This prevents errors that Xcode falsely recognizes as array slice problems.
Edit: Use implicitly unwrapped optionals to remove the need to force unwrap .context every time it is being used.

As an alternative for those who would like to make use of XCode's modern approach to NSManagedObject file generation, I have created a DecoderWrapper class to expose a Decoder object which I then use within my object which conforms to a JSONDecoding protocol:
class DecoderWrapper: Decodable {
let decoder:Decoder
required init(from decoder:Decoder) throws {
self.decoder = decoder
}
}
protocol JSONDecoding {
func decodeWith(_ decoder: Decoder) throws
}
extension JSONDecoding where Self:NSManagedObject {
func decode(json:[String:Any]) throws {
let data = try JSONSerialization.data(withJSONObject: json, options: [])
let wrapper = try JSONDecoder().decode(DecoderWrapper.self, from: data)
try decodeWith(wrapper.decoder)
}
}
extension MyCoreDataClass: JSONDecoding {
enum CodingKeys: String, CodingKey {
case name // For example
}
func decodeWith(_ decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
}
}
This is probably only useful for models without any non-optional attributes, but it solves my problem of wanting to use Decodable but also manage relationships and persistence with Core Data without having to manually create all my classes / properties.
Edit: Example of it in use
If I have a json object:
let myjson = [ "name" : "Something" ]
I create the object in Core Data (force cast here for brevity):
let myObject = NSEntityDescription.insertNewObject(forEntityName: "MyCoreDataClass", into: myContext) as! MyCoreDataClass
And I use the extension to have the object decode the json:
do {
try myObject.decode(json: myjson)
}
catch {
// handle any error
}
Now myObject.name is "Something"

Related

Decoding objects without knowing their type first

There is a likelihood this is an XY problem, I am open to these suggestions as well !
I am trying to work with Minecraft save data. Minecraft encodes Entities (basically anything that is not strictly a block) with their type inside an id property . The file then contains a big array of entities, which I want to decode and instantiate.
The problem is that, using Decodable, I must know an object's type before I start instantiating it like container.decode(Zombie.self). I can't figure out how to create a function that would read the id and return the right type of entity ?
I think this explains what I need better than any explanation could :
//Entity objects don't actually store their ID since re-encoding it is trivial.
protocol Entity : Decodable {var someProperty : Int {get set}}
struct Zombie : Entity {var someProperty : Int}
struct Skeleton : Entity {var someProperty : Int}
//Using JSON instead of SNBT so we can use JSONDecoder
let jsonData = """
[
{
"id":"zombie",
"someProperty":"3"
},
{
"id" : "skeleton",
"someProperty":"3"
}
]
"""
struct EntityList : Decodable {
var list : [any Entity] = []
init(from decoder : Decoder) throws {
var container = try decoder.unkeyedContainer()
//What should we put here ?
}
}
let decoder = JSONDecoder()
let entityList = try decoder.decode(EntityList.self, from: Data(jsonData.utf8))
//entityList should be [Zombie, Skeleton]
At the moment I'm looking into the Factory pattern, maybe that's an interesting lead ? In any case, thank you for your help !
( Please note this question has nothing to do with decoding the actual binary contents of the file, it was honestly quite hard to do but I already have a working Encoder / Decoder. It is only about unpacking those contents, hence why I just used JSON in the example above, since we have a common Decoder for that. )
I honestly haven't used the new any syntax enough to know if that can help but I have done what you're trying to do numerous times and here is how I do it.
Set up the data first
We first declare what a Zombie and a Skeleton are. They could just inherit from a protocol or they could be separate structs...
struct Zombie: Decodable {
let someProperty: Int
}
struct Skeleton: Decodable {
let someProperty: Int
let skeletonSpecificProperty: String
}
Then we can turn your array of [anyEntityType] into a homogeneous array by using an enum and embedding the entities into it...
enum Entity: Decodable {
case zombie(Zombie)
case skeleton(Skeleton)
}
Decode the enum given your JSON structure
We have to provide a custom decoder for the Entity type...
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKeys.self)
// First get the `id` value from the JSON object
let type = try container.decode(String.self, forKey: .id)
// check the value for each type of entity we can decode
switch type {
// for each value of `id` create the related type
case "zombie":
let zombie = try Zombie(from: decoder)
self = .zombie(zombie)
case "skeleton":
let skeleton = try Skeleton(from: decoder)
self = .skeleton(skeleton)
default:
// throw an error here... unsupported type or something
}
}
This should now let you decode an array of Entities from JSON into an [Entity] array.
Deal with "unknown" types
There is an extra step required for dealing with the "unknown" types. For instance, in the code above. If the JSON contains "id": "creeper" this will error as it can't deal with that. And you'll end up with your whole array failing to decode.
I've created a couple of helper functions that help with that...
If you create an object like...
struct Minecraft: Decodable {
let entities: [Entity]
enum RootKeys: String, CodingKey {
case entities
}
}
And these helpers...
extension KeyedDecodingContainer {
func decodeAny<T: Decodable>(_ type: T.Type, forKey key: K) throws -> [T] {
var items = try nestedUnkeyedContainer(forKey: key)
var itemsArray: [T] = []
while !items.isAtEnd {
guard let item = try? items.decode(T.self) else {
try items.skip()
continue
}
itemsArray.append(item)
}
return itemsArray
}
}
private struct Empty: Decodable { }
extension UnkeyedDecodingContainer {
mutating func skip() throws {
_ = try decode(Empty.self)
}
}
You can create a custom decoder for the Minecraft type like this...
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKeys.self)
self.entities = try container.decodeAny(Entity.self, forKey: .entities)
}

Cannot decode JSON using Core Data class [duplicate]

Codable seems a very exciting feature. But I wonder how we can use it in Core Data? In particular, is it possible to directly encode/decode a JSON from/to a NSManagedObject?
I tried a very simple example:
and defined Foo myself:
import CoreData
#objc(Foo)
public class Foo: NSManagedObject, Codable {}
But when using it like this:
let json = """
{
"name": "foo",
"bars": [{
"name": "bar1",
}], [{
"name": "bar2"
}]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let foo = try! decoder.decode(Foo.self, from: json)
print(foo)
The compiler failed with this errror:
super.init isn't called on all paths before returning from initializer
and the target file was the file that defined Foo
I guess I probably did it wrong, since I didn't even pass a NSManagedObjectContext, but I have no idea where to stick it.
Does Core Data support Codable?
You can use the Codable interface with CoreData objects to encode and decode data, however it's not as automatic as when used with plain old swift objects. Here's how you can implement JSON Decoding directly with Core Data objects:
First, you make your object implement Codable. This interface must be defined on the object, and not in an extension. You can also define your Coding Keys in this class.
class MyManagedObject: NSManagedObject, Codable {
#NSManaged var property: String?
enum CodingKeys: String, CodingKey {
case property = "json_key"
}
}
Next, you can define the init method. This must also be defined in the class method because the init method is required by the Decodable protocol.
required convenience init(from decoder: Decoder) throws {
}
However, the proper initializer for use with managed objects is:
NSManagedObject.init(entity: NSEntityDescription, into context: NSManagedObjectContext)
So, the secret here is to use the userInfo dictionary to pass in the proper context object into the initializer. To do this, you'll need to extend the CodingUserInfoKey struct with a new key:
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")
}
Now, you can just as the decoder for the context:
required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }
guard let entity = NSEntityDescription.entity(forEntityName: "MyManagedObject", in: context) else { fatalError() }
self.init(entity: entity, in: context)
let container = decoder.container(keyedBy: CodingKeys.self)
self.property = container.decodeIfPresent(String.self, forKey: .property)
}
Now, when you set up the decoding for Managed Objects, you'll need to pass along the proper context object:
let data = //raw json data in Data object
let context = persistentContainer.newBackgroundContext()
let decoder = JSONDecoder()
decoder.userInfo[.context] = context
_ = try decoder.decode(MyManagedObject.self, from: data) //we'll get the value from another context using a fetch request later...
try context.save() //make sure to save your data once decoding is complete
To encode data, you'll need to do something similar using the encode protocol function.
CoreData is its own persistence framework and, per its thorough documentation, you must use its designated initializers and follow a rather specific path to creating and storing objects with it.
You can still use Codable with it in limited ways just as you can use NSCoding, however.
One way is to decode an object (or a struct) with either of these protocols and transfer its properties into a new NSManagedObject instance you've created per Core Data's docs.
Another way (which is very common) is to use one of the protocols only for a non-standard object you want to store in a managed object's properties. By "non-standard", I mean anything thst doesn't conform to Core Data's standard attribute types as specified in your model. For example, NSColor can't be stored directly as a Managed Object property since it's not one of the basic attribute types CD supports. Instead, you can use NSKeyedArchiver to serialize the color into an NSData instance and store it as a Data property in the Managed Object. Reverse this process with NSKeyedUnarchiver. That's simplistic and there is a much better way to do this with Core Data (see Transient Attributes) but it illustrates my point.
You could also conceivably adopt Encodable (one of the two protocols that compose Codable - can you guess the name of the other?) to convert a Managed Object instance directly to JSON for sharing but you'd have to specify coding keys and your own custom encode implementation since it won't be auto-synthesized by the compiler with custom coding keys. In this case you'd want to specify only the keys (properties) you want to be included.
Hope this helps.
Swift 4.2:
Following casademora's solution,
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError() }
should be
guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }.
This prevents errors that Xcode falsely recognizes as array slice problems.
Edit: Use implicitly unwrapped optionals to remove the need to force unwrap .context every time it is being used.
As an alternative for those who would like to make use of XCode's modern approach to NSManagedObject file generation, I have created a DecoderWrapper class to expose a Decoder object which I then use within my object which conforms to a JSONDecoding protocol:
class DecoderWrapper: Decodable {
let decoder:Decoder
required init(from decoder:Decoder) throws {
self.decoder = decoder
}
}
protocol JSONDecoding {
func decodeWith(_ decoder: Decoder) throws
}
extension JSONDecoding where Self:NSManagedObject {
func decode(json:[String:Any]) throws {
let data = try JSONSerialization.data(withJSONObject: json, options: [])
let wrapper = try JSONDecoder().decode(DecoderWrapper.self, from: data)
try decodeWith(wrapper.decoder)
}
}
extension MyCoreDataClass: JSONDecoding {
enum CodingKeys: String, CodingKey {
case name // For example
}
func decodeWith(_ decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
}
}
This is probably only useful for models without any non-optional attributes, but it solves my problem of wanting to use Decodable but also manage relationships and persistence with Core Data without having to manually create all my classes / properties.
Edit: Example of it in use
If I have a json object:
let myjson = [ "name" : "Something" ]
I create the object in Core Data (force cast here for brevity):
let myObject = NSEntityDescription.insertNewObject(forEntityName: "MyCoreDataClass", into: myContext) as! MyCoreDataClass
And I use the extension to have the object decode the json:
do {
try myObject.decode(json: myjson)
}
catch {
// handle any error
}
Now myObject.name is "Something"

Save a Struct to Core Data (NOT a CLASS) [duplicate]

Codable seems a very exciting feature. But I wonder how we can use it in Core Data? In particular, is it possible to directly encode/decode a JSON from/to a NSManagedObject?
I tried a very simple example:
and defined Foo myself:
import CoreData
#objc(Foo)
public class Foo: NSManagedObject, Codable {}
But when using it like this:
let json = """
{
"name": "foo",
"bars": [{
"name": "bar1",
}], [{
"name": "bar2"
}]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let foo = try! decoder.decode(Foo.self, from: json)
print(foo)
The compiler failed with this errror:
super.init isn't called on all paths before returning from initializer
and the target file was the file that defined Foo
I guess I probably did it wrong, since I didn't even pass a NSManagedObjectContext, but I have no idea where to stick it.
Does Core Data support Codable?
You can use the Codable interface with CoreData objects to encode and decode data, however it's not as automatic as when used with plain old swift objects. Here's how you can implement JSON Decoding directly with Core Data objects:
First, you make your object implement Codable. This interface must be defined on the object, and not in an extension. You can also define your Coding Keys in this class.
class MyManagedObject: NSManagedObject, Codable {
#NSManaged var property: String?
enum CodingKeys: String, CodingKey {
case property = "json_key"
}
}
Next, you can define the init method. This must also be defined in the class method because the init method is required by the Decodable protocol.
required convenience init(from decoder: Decoder) throws {
}
However, the proper initializer for use with managed objects is:
NSManagedObject.init(entity: NSEntityDescription, into context: NSManagedObjectContext)
So, the secret here is to use the userInfo dictionary to pass in the proper context object into the initializer. To do this, you'll need to extend the CodingUserInfoKey struct with a new key:
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")
}
Now, you can just as the decoder for the context:
required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }
guard let entity = NSEntityDescription.entity(forEntityName: "MyManagedObject", in: context) else { fatalError() }
self.init(entity: entity, in: context)
let container = decoder.container(keyedBy: CodingKeys.self)
self.property = container.decodeIfPresent(String.self, forKey: .property)
}
Now, when you set up the decoding for Managed Objects, you'll need to pass along the proper context object:
let data = //raw json data in Data object
let context = persistentContainer.newBackgroundContext()
let decoder = JSONDecoder()
decoder.userInfo[.context] = context
_ = try decoder.decode(MyManagedObject.self, from: data) //we'll get the value from another context using a fetch request later...
try context.save() //make sure to save your data once decoding is complete
To encode data, you'll need to do something similar using the encode protocol function.
CoreData is its own persistence framework and, per its thorough documentation, you must use its designated initializers and follow a rather specific path to creating and storing objects with it.
You can still use Codable with it in limited ways just as you can use NSCoding, however.
One way is to decode an object (or a struct) with either of these protocols and transfer its properties into a new NSManagedObject instance you've created per Core Data's docs.
Another way (which is very common) is to use one of the protocols only for a non-standard object you want to store in a managed object's properties. By "non-standard", I mean anything thst doesn't conform to Core Data's standard attribute types as specified in your model. For example, NSColor can't be stored directly as a Managed Object property since it's not one of the basic attribute types CD supports. Instead, you can use NSKeyedArchiver to serialize the color into an NSData instance and store it as a Data property in the Managed Object. Reverse this process with NSKeyedUnarchiver. That's simplistic and there is a much better way to do this with Core Data (see Transient Attributes) but it illustrates my point.
You could also conceivably adopt Encodable (one of the two protocols that compose Codable - can you guess the name of the other?) to convert a Managed Object instance directly to JSON for sharing but you'd have to specify coding keys and your own custom encode implementation since it won't be auto-synthesized by the compiler with custom coding keys. In this case you'd want to specify only the keys (properties) you want to be included.
Hope this helps.
Swift 4.2:
Following casademora's solution,
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError() }
should be
guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }.
This prevents errors that Xcode falsely recognizes as array slice problems.
Edit: Use implicitly unwrapped optionals to remove the need to force unwrap .context every time it is being used.
As an alternative for those who would like to make use of XCode's modern approach to NSManagedObject file generation, I have created a DecoderWrapper class to expose a Decoder object which I then use within my object which conforms to a JSONDecoding protocol:
class DecoderWrapper: Decodable {
let decoder:Decoder
required init(from decoder:Decoder) throws {
self.decoder = decoder
}
}
protocol JSONDecoding {
func decodeWith(_ decoder: Decoder) throws
}
extension JSONDecoding where Self:NSManagedObject {
func decode(json:[String:Any]) throws {
let data = try JSONSerialization.data(withJSONObject: json, options: [])
let wrapper = try JSONDecoder().decode(DecoderWrapper.self, from: data)
try decodeWith(wrapper.decoder)
}
}
extension MyCoreDataClass: JSONDecoding {
enum CodingKeys: String, CodingKey {
case name // For example
}
func decodeWith(_ decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
}
}
This is probably only useful for models without any non-optional attributes, but it solves my problem of wanting to use Decodable but also manage relationships and persistence with Core Data without having to manually create all my classes / properties.
Edit: Example of it in use
If I have a json object:
let myjson = [ "name" : "Something" ]
I create the object in Core Data (force cast here for brevity):
let myObject = NSEntityDescription.insertNewObject(forEntityName: "MyCoreDataClass", into: myContext) as! MyCoreDataClass
And I use the extension to have the object decode the json:
do {
try myObject.decode(json: myjson)
}
catch {
// handle any error
}
Now myObject.name is "Something"

How to use Codable and Coredata together?

I am trying to implement Codable with Coredata. I've tried following the following answer, but still have had no luck.
How to use swift 4 Codable in Core Data?
The error/problem I am having is my project is continuing to say: "Argument type 'User' does not conform to expected type 'Encodable' whenever I try to encode or decode the object.
I have created the Entity in CoreData and made NSManagedObject subclasses:
import Foundation
import CoreData
#objc(User)
public class User: NSManagedObject, Codable {
// MARK: - Codable setUp
enum CodingKeys: String, CodingKey {
case fullname
case email
case zipcode
case usertype = "user_type"
}
// MARK: - Decoding the data
required convenience init(from decoder: Decoder) {
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else {NSLog("Error: with User context!")
return
}
guard let entity = NSEntityDescription.entity(forEntityName: "User", in: context) else {
NSLog("Error with user enity!")
return
}
self.init(entity: entity, in: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
fullname = try values.decode(String.self, forkey: .fullname)
email = try values.decode(String.self, forkey: .email)
zipcode = try values.decode(String.self, forkey: .zipcode)
userType = try values.decode(String.self, forkey: .userType)
}
// MARK: - Encoding the data
func encode(to encoder: Encoder) throws {
var container = try encoder.container(keyedBy: CodingKeys.self)
try container.encode(fullname, forkey: .fullname)
try container.encode(email, forkey: .email)
try container.encode(usertype, forkey: .usertype)
try container.encode(zipcode, forkey: .zipcode)
}
}
// This helps with decoding
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")
}
When I try to decode the object and save the user to firebase I get a warning that says "In argument type 'User.Type', 'User' does not conform to expected type 'Decodable'
When I try to encode I get a warning that says "Argument type 'User' does not conform to expected type 'Encodable'
User' does not conform to expected type 'Decodable
occurs due to a typo (case sensitivity matters):
case usertype ... vs. ...forKey: .userType)
and the encode/decode(:forKey:) methods have a capital K
Your Problem might not be related to CoreData/Codable, but rather to an imported User model, that in fact does not conform to Codable.
Firebase defines a User in their FirebaseAuth.framework (Reference). If you import Firebase at the top of your file, it will also import that User declaration. IMO this is bad API design by Firebase, because many apps that also use FirebaseAuth will have a User model with some custom properties at some point...
If you don't need FirebaseAuth in that file, just import the more specific Firebase framework, e.g. import FirebaseDatabase to avoid importing their User model.
Otherwise, to help the compiler selecting your User model instead of the imported model, you can explicitly prepend User.self with your module name: YourModule.User.self.

How can I decode when I don't know the type, with class inheritance?

I have a base class Action, which is an Operation. It has a bunch of crufty Operation stuff in it (KVO and all that). The base class itself doesn't actually need to encode/decode anything.
class Action : Operation, Codable {
var _executing = false
...
}
I have a bunch of Action sub-classes, like DropboxUploadAction, which are directly instantiated with an Input struct they define:
let actionInput = DropboxUploadAction.Input.init(...)
ActionManager.shared.run(DropboxUploadAction.init(actionInput, data: binaryData), completionBlock: nil)
Here's what the subclasses look like:
class DropboxUploadAction : Action {
struct Input : Codable {
var guid: String
var eventName: String
var fileURL: URL?
var filenameOnDropbox: String
var share: Bool
}
struct Output : Codable {
var sharedFileLink: String?
var dropboxPath: String?
}
var input: Input
var output: Output
...
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
input = try values.decode(Input.self, forKey: .input)
output = try values.decode(Output.self, forKey: .output)
let superDecoder = try values.superDecoder()
try super.init(from: superDecoder)
}
fileprivate enum CodingKeys: String, CodingKey {
case input
case output
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(input, forKey: .input)
try container.encode(output, forKey: .output)
try super.encode(to: container.superEncoder())
}
}
When some situations occur such as a loss of internet connectivity, these classes need to be serialized to disk for later. That's fine, because at the time I have references to them and can encode them with JSONEncoder().encode(action), no problem.
But later when I want to deserialize them, I need to specify the type of the class and I don't know what it is. I have some data and I know it can be decoded to a class that inherits from Action, but I don't know which subclass it is. I'm loathe to encode that in the filename. Is there some way to decode it as the base class Action, then in the decode() method of Action, somehow detect the proper class and redirect?
In the past I've used NSKeyedUnarchiver.setClass() to handle this. But I don't know how to do that with Swift 4's Codable, and I understand that NSCoding is deprecated now so I shouldn't use NSKeyedUnarchiver anymore...
If it helps: I have a struct Types : OptionSet, Codable which each subclass returns, so I don't have to use the name of the class as its identity.
Thanks for any help!
Uhhh NSCoding isn't deprecated. We still use it when instantiating UIViewControllers from storyboard via init(coder:).
Also, if you still don't want to use NSCoding, you can just store the Input, Output and Types to a struct and serialize that to disk instead.
struct SerializedAction {
let input: Input
let output: Output
let type: Type
}
When needed, you can decode that and decide the correct Action to initialize with your input/output via the type property.
class DropboxAction: Action {
...
init(input: Input, output: Output) {
...
}
}
You don't necessarily need to encode the entire Action object.