In this chapter, you took advantage of automatic conformance to the Codable protocol and did not need to implement encode(to:) or init(from:) yourself. What if you had some property that was not codable? Let’s take a look at how to implement those two methods.
The code you add in this section will conflict with later chapters, so create a copy of your LootLogger project to work in, as you do for challenges. Open Item.swift. Create a new enumeration to describe the category of an item and add a property to reference the category.
Listing 13.11 Adding a Category
enumeration (Item.swift
)
enum Category { case electronics case clothing case book case other } var category = Category.other
(For brevity, the category property is given a default value. In practice, you would probably want to add an additional parameter to the initializer to allow the category to be customized.)
With the introduction of the category property, Codable conformance is no longer automatic. The first thing you need to do to regain codable functionality is to define the keys that will be used during encoding. You can think of these as being like the keys to a dictionary.
In Item.swift, define another enumeration named CodingKeys that conforms to the CodingKey protocol.
Listing 13.12 Adding a CodingKeys
enumeration (Item.swift
)
enum CodingKeys: String, CodingKey { case name case valueInDollars case serialNumber case dateCreated case category }
The CodingKeys enumeration has a raw value of type String, as indicated in its declaration. This means that every case is associated with a string of the same name as the case. (You will learn about enumerations with raw values in Chapter 20.)
It also conforms to the CodingKey protocol. The CodingKey protocol effectively has one requirement: a stringValue for each of the keys. Since the CodingKeys enum is backed by a String raw value, this requirement is automatically satisfied.
With the CodingKeys enumeration created, you can now implement encode(to:).
Listing 13.13 Implementing encode(to:)
(Item.swift
)
func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(valueInDollars, forKey: .valueInDollars) try container.encode(serialNumber, forKey: .serialNumber) try container.encode(dateCreated, forKey: .dateCreated) switch category { case .electronics: try container.encode("electronics", forKey: .category) case .clothing: try container.encode("clothing", forKey: .category) case .book: try container.encode("book", forKey: .category) case .other: try container.encode("other", forKey: .category) } }
First, you create a container. Generally, you will want a keyed container that acts like a dictionary. You specify which keys the container supports by passing it the CodingKeys enumeration.
After the container is created, you encode each piece of data that you want to persist. The encode(_:forKey:) method can fail if the value passed in is invalid for the current context, so it must be annotated with try
.
Encoding the original Item properties is pretty straightforward. For the new category property, you cannot just encode the property itself. After all, that is the issue you are addressing here: Category is not Codable itself, so you must “convert” it to a type that is. To do this, you switch over the category and encode a string for each case.
With the encode(to:) method implemented, let’s implement init(from:).
Listing 13.14 Implementing init(from:)
(Item.swift
)
required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) valueInDollars = try container.decode(Int.self, forKey: .valueInDollars) serialNumber = try container.decode(String?.self, forKey: .serialNumber) dateCreated = try container.decode(Date.self, forKey: .dateCreated) let categoryString = try container.decode(String.self, forKey: .category) switch categoryString { case "electronics": category = .electronics case "clothing": category = .clothing case "book": category = .book case "other": category = .other default: category = .other } }
This initializer’s role is to pull out the data you need from a container. Once again, you specify which keys are associated with the container. Then you decode the properties, specifying the type and key for each. For the category property, you need to decode the string that was encoded earlier, check the contents of that string, and then assign the corresponding enumeration case.
Build the application and notice that the errors are addressed. You have now restored codable functionality to Item.