Codable NSManagedObject (Core Data) and CLLocation in Swift 4

Before entering the topic, let’s talk about codable a little!

Codable

Codable is a new feature in Swift 4. It is a type alias for Decodable protocol and Encodable protocol.

typealias Codable = Decodable & Encodable

A enum/struct/class conformed by Decodable can be deserialized. For example, a json object is deserialized to a struct.

// Declaration
struct Person: Decodable {
let name: String
}
// Deserialization
let jsonString = """
{
"name": "Zheng-Xiang Ke"
}
"""
if let jsonData = jsonString.data(using: .utf8) {
let person = try? JSONDecoder().decode(Person.self, from: jsonData)
}

A enum/struct/class conformed by Encodable can be serialized. For example, a struct is serialized to a json object.

// Declaration
struct Person: Encodable {
let name: String
}
// Serialization
let person = Person(name: "Zheng-Xiang Ke")
let jsonData = try? JSONEncoder().encode(person)

The above examples are very easy, but show 2 key points.

A custom type is codable iff (if and only if) all of its properties are codable.

String, Int, Double, Data, and URL are built-in codable types, so the Person is codable.

Let’s see a counter example.

struct Asset {
let property: Int
}
struct Person: Codable {
let name: String
// Asset is not codable, so Person cannot conform codable
let asset: Asset
}

In addition, the built-in types such as Array, Dictionary, and Optional conform to Codable whenever they contain codable types.

2.

The default way to encode/decode are automatically generated by the compiler.

It means you don’t need to care about coding key mapping! The default way to encode/decode see the property’s name as its key.

// Declaration
struct Person: Codable {
let name: String
}
let person = Person(name: "Zheng-Xiang Ke")
// Serialization for person: {"name": "Zheng-Xiang Ke"}

Customization

If you want to customize or omit some coding keys, then a special nested enumeration named CodingKeys that conforms to the CodingKey protocol is declared.

// Declaration
struct Person: Codable {
let name: String
let gender: String
    enum CodingKeys: String, CodingKey {
case name = "username"
}
}
let person = Person(name: "Zheng-Xiang Ke", gender: "male")
// Serialization for person: {"username": "Zheng-Xiang Ke"}

You also can define your own encoding/decoding logic to overwrite the default way to encode/decode a codable type.

// Declaration
struct Person {
let name: String
let gender: String
enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
}
// Decodable
extension Person: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let firstName = try values.decode(String.self, forKey: .firstName)
let lastName = try values.decode(String.self, forKey: .lastName)
name = firstName + " " + lastName
}
}
// Encodable
extension Person: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let nameComponents = name.components(separatedBy: " ")
try container.encode(nameComponents[0], forKey: .firstName)
try container.encode(nameComponents[1], forKey: .lastName)
}
}
let person = Person(name: "Zheng-Xiang Ke")
// Serialization for person: {"firstName": "Zheng-Xiang", "lastName": "Ke"}

To sum up, there 3 steps to customize your logic to encode/decode a codable type.

Step 1. Define CodingKeys enumeration
Step 2. Conform Encodable protocol
Step 3. Conform Decodable protocol

Codable NSManagedObject

If your app maintains a local database and communicates with your web service, then making NSManagedObject conform to codable protocol is a easy and good way.

When you give it a try, you will find auto-generated encoding/decoding way don’t work! So…

Making NSManagedObject conform to Codable protocol must be manual.

However, there are some constraints to make NSManagedObject conform to codable protocol. Let’s take a look step by step.

Step 1. Define CodingKeys enumeration

class Person: NSManagedObject {
@NSManaged var name: String

enum CodingKeys: String, CodingKey {
case name
}
}

Step 2. Conform Encodable protocol

extension Person: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
}
}

Step 3. Conform Decodable protocol

There are some constraints in this step.

init(from:): is the required initializer for Decodable. However, according to the Swift documentation,

Extensions can add new convenience initializers to a class, but they cannot add new designated initializers or deinitializers to a class.

Therefore, we must conform Decodable in the original class instead of extension. In addition, init(from:): is declared with the following modifiers:

* required: init(from:): is a required initializer, so the required modifier is required.
* convenience: The super class NSManagedObject doesn’t implement the designated initializers, so app will crash once super.init(entity: , insertInto:) is invoked. So we need to leverage self.init(entity: , insertInto:)

However, how to get NSManagedObjectContext in init(from:):?

The secret is userInfo property in Decoder. You can leverage userInfo to pass a proper NSManagedObjectContext to the init(from:):.

extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")
}
class Person: NSManagedObject, Decodable {
@NSManaged var name: String

enum CodingKeys: String, CodingKey {
case name
}
required convenience init(from decoder: Decoder) throws {
// Create NSEntityDescription with NSManagedObjectContext
guard let contextUserInfoKey = CodingUserInfoKey.context,
let managedObjectContext = decoder.userInfo[contextUserInfoKey] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: ENTITY_NAME, in: managedObjectContext) else {
            fatalError("Failed to decode Person!")
}
        self.init(entity: entity, insertInto: nil)

// Decode
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)

}
}
let data = ...
let context = persistentContainer.newBackgroundContext()
let decoder = JSONDecoder()
if let context = CodingUserInfoKey.context {
decoder.userInfo[context] = 1
}
_ = try decoder.decode(Person.self, from: data)

In this way, a codable NSManagedObject type is implemented successfully. 👍


Codable CLLocation

CLLocation is a built-in uncodable type, so we must do something to make CLLocation conform Codable protocol.

As state above, the Decodable conformation is a big headache!!! 🤕 However, the logic is the same. Let’s take a look!

Step 1 & 2. Define CodingKeys enumeration & Conform Encodable protocol

extension CLLocation: Encodable {
enum CodingKeys: String, CodingKey {
case latitude
case longitude
case altitude
case horizontalAccuracy
case verticalAccuracy
case speed
case course
case timestamp
}
    public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(coordinate.latitude, forKey: .latitude)
try container.encode(coordinate.longitude, forKey: .longitude)
try container.encode(altitude, forKey: .altitude)
try container.encode(horizontalAccuracy, forKey: .horizontalAccuracy)
try container.encode(verticalAccuracy, forKey: .verticalAccuracy)
try container.encode(speed, forKey: .speed)
try container.encode(course, forKey: .course)
try container.encode(timestamp, forKey: .timestamp)
}
}

Step 3. Conform Decodable protocol

In actually, CLLocation cannot conform Decodable protocol if it is not a built-in decodable type because we cannot leverage Extension to implement init(from:):, which is required by Decodable as mentioned previously.

However, if a struct/class owns a CLLocation object, it can be codable through a little tricky way.

A struct Location is declared to make it.

struct Location: Codable {
let latitude: CLLocationDegrees
let longitude: CLLocationDegrees
let altitude: CLLocationDistance
let horizontalAccuracy: CLLocationAccuracy
let verticalAccuracy: CLLocationAccuracy
let speed: CLLocationSpeed
let course: CLLocationDirection
let timestamp: Date
}
extension CLLocation {
convenience init(model: Location) {
self.init(coordinate: CLLocationCoordinate2DMake(model.latitude, model.longitude), altitude: model.altitude, horizontalAccuracy: model.horizontalAccuracy, verticalAccuracy: model.verticalAccuracy, course: model.course, speed: model.speed, timestamp: model.timestamp)
}
}
struct Person {
let name: String
let location: CLLocation
    enum CodingKeys: String, CodingKey {
case name
case location
}
}
extension Person: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let name = try values.decode(String.self, forKey: .name)
let locationModel = try values.decode(Location.self, forKey: .location)
location = CLLocation(model: locationModel)
}
}

This article introduces Codable and shows special types conformed to Codable such as NSManagedObject and CLLocation. I took some time to come out the solution and hope this article can reduce your time.

Feel free to leave comments if you have any doubts. 😁