45

Encoding and Decoding in Swift [FREE]

 4 years ago
source link: https://www.tuicool.com/articles/uyeEniU
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A common task for iOS apps is to save data and send it over the network. But before you can do that, you need to convert the data to a suitable format through a process called encoding or serialization .

3MbYzar.png!web

You’ll also need to convert the saved data sent over the network to a suitable format before using it in your app. This reverse process is called decoding or deserialization .

eYbIVjZ.png!web

In this tutorial, you’ll learn all you need to know about encoding and decoding in Swift by managing your very own toy store. You’ll explore the following topics along the way:

  • Switching between snake case and camel case formats.
  • Defining custom coding keys.
  • Working with keyed, unkeyed and nested containers.
  • Handling nested types, dates, subclasses and polymorphic types.

There’s quite a lot to cover, so it’s time to get started! :]

Note : This tutorial assumes you have a basic knowledge of JSON . Check out this cheat sheet if you need a quick overview.

Getting Started

Download the starter playground using the Download Materials link at the top or bottom of the tutorial.

Make sure the Project navigator is visible in Xcode by going to View ▸ Navigators ▸ Show Project Navigator . Open Nested types .

Add Codable conformance to Toy and Employee :

struct Toy: Codable {
  ...
}
struct Employee: Codable {
  ...
}

Codable isn’t a protocol on it’s own, but an alias for two other protocols: Encodable and Decodable . As you might guess, those two protocols declare that types can be encoded to and decoded from a different format.

You don’t need to do anything more, because all the stored properties of both Toy and Employee are codable. Many basic types in the Swift Standard Library and Foundation types (for example, String and URL ) are codable by default.

Note : You can encode codable types to various formats such as Property Lists (PLists) , XML or JSON , but for this tutorial you’ll only work with JSON.

Add a JSONEncoder and a JSONDecoder to handle JSON encoding and decoding of toys and employees:

let encoder = JSONEncoder()
let decoder = JSONDecoder()

That’s all you need to work with JSON. Time for your first encoding and decoding challenge!

Encoding and Decoding Nested Types

Employee contains a Toy property — it’s a nested type . The JSON structure of your encoded employee matches the Employee struct:

{
  "name" : "John Appleseed",
  "id" : 7,
  "favoriteToy" : {
    "name" : "Teddy Bear"
  }
}
public struct Employee: Codable {
  var name: String
  var id: Int
  var favoriteToy: Toy
}

The JSON nests name inside favoriteToy and all the JSON keys are the same as the Employee and Toy stored properties, so you can easily understand the JSON structure based on your data types hierarchy. If your property names match your JSON field names, and your properties are all Codable , then you can convert to or from JSON very easily. You’ll try that now.

The Gifts department gives employees their favorite toys as birthday gifts. Add the following code to send your employee’s data to the Gifts department:

// 1
let data = try encoder.encode(employee)
// 2
let string = String(data: data, encoding: .utf8)!

Here’s how this code works:

  1. Encode employee to JSON with encode(_:) (I told you it was easy!).
  2. Create a string from the encoded data to visualize it.

Note : Press Shift-Return to run the playground up to your current line, or click the blue play button. To see results, you can print values to the debugger console or click the Show Result button in the results sidebar.

The encoding process generates valid data, so the Gifts department can recreate the employee:

let sameEmployee = try decoder.decode(Employee.self, from: data)

Here, you’ve used decode(_:from:) to decode data back to Employee … and you’ve made your employee very happy. Press the blue play button to run the Playground and see the results.

Time for your next challenge!

Switching Between Snake Case and Camel Case Formats

The Gifts department API has switched from camel case (which looksLikeThis ) to snake case (which looks_like_this_instead ) to format keys for its JSON.

But all of the stored properties of Employee and Toy use camel case only! Fortunately, Foundation has you covered.

Open Snake case vs camel case and add the following code just after the encoder and decoder are created, before they get used:

encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase

Here, you set keyEncodingStrategy to .convertToSnakeCase to encode employee . You also set keyDecodingStrategy to .convertFromSnakeCase to decode snakeData .

Run the playground and inspect snakeString . The encoded employee looks like this in this case (pun intended):

{
  "name" : "John Appleseed",
  "id" : 7,
  "favorite_toy" : {
    "name" : "Teddy Bear"
  }
}

The formatting in JSON is now favorite_toy and you’ve transformed it back to favoriteToy in the Employee struct. You’ve saved the (employee’s birth)day again! :]

announcement.png

Working With Custom JSON Keys

The Gifts department has changed its API again to use different JSON keys than your Employee and Toy stored properties use:

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : {
    "name" : "Teddy Bear"
  }
}

Now, the API replaces favoriteToy with gift .

This means that the field names in the JSON will no longer match up with the property names in your type. You can define custom coding keys to supply coding names for your properties. You do this by adding a special enum to your type. Open Custom coding keys and add this code inside the Employee type:

enum CodingKeys: String, CodingKey {
  case name, id, favoriteToy = "gift"
}

CodingKeys is the special enum mentioned above. It conforms to CodingKey and has String raw values. Here’s where you map favoriteToy to gift .

If this enum exists, only the cases present here will be used for encoding and decoding, so even if your property doesn’t require mapping, it has to be included in the enum, as name and id are here.

Run the playground and look at the encoded string value — you’ll see the new field name in use. The JSON doesn’t depend on your stored properties anymore, thanks to custom coding keys.

Time for your next challenge!

Working With Flat JSON Hierarchies

Now the Gifts department’s API doesn’t want any nested types in its JSON, so their code looks like this:

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : "Teddy Bear"
}

This doesn’t match your model structure, so you need to write your own encoding logic and describe how to encode each Employee and Toy stored property.

To start, open Keyed containers . You’ll see an Employee type which is declared as Encodable . It’s also declared Decodable in an extension. This split is to keep the free member-wise initializer you get with Swift struct s. If you declare an init method in the main definition, you lose that. Add this code inside Employee :

// 1
enum CodingKeys: CodingKey {
  case name, id, gift
}

func encode(to encoder: Encoder) throws {
  // 2
  var container = encoder.container(keyedBy: CodingKeys.self)
  // 3  
  try container.encode(name, forKey: .name)
  try container.encode(id, forKey: .id)
  // 4
  try container.encode(toy.name, forKey: .gift)
}

For simple cases like you’ve seen above, encode(to:) is automatically implemented for you by the compiler. Now, you’re doing it yourself. Here’s what the code is doing:

KeyedEncodingContainer

Run the playground and inspect the value of the encoded string – it will match the JSON at the top of this section. Being able to choose which properties to encode for which keys gives you a lot of flexibility.

The decoding process is the opposite of the encoding process. Replace the scary fatalError("To do") with this:

// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
// 2
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
// 3
let gift = try container.decode(String.self, forKey: .gift)
favoriteToy = Toy(name: gift)

As with encoding, for simple cases init(from:) is made automatically for you by the compiler, but here you’re doing it yourself. Here’s what the code is doing:

Toy

Add a line to recreate an employee from your flat JSON:

let sameEmployee = try decoder.decode(Employee.self, from: data)

This time, you have chosen which properties to decode for which keys and had the chance to do further work during decoding. Manual encoding and decoding is powerful and gives you flexibility. You’ll learn more about this in the next challenges.

Working With Deep JSON Hierarchies

The Gifts department wants to make sure that the employees’ birthday gifts can only be toys, so its API generates JSON that looks like this:

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : {
    "toy" : {
      "name" : "Teddy Bear"
    }
  }
}

You nest name inside toy and toy inside gift . The JSON structure adds an extra level of indentation compared to the Employee hierarchy, so you need to use nested keyed containers for gift in this case.

Open Nested keyed containers and add the following code to Employee :

// 1  
enum CodingKeys: CodingKey {  
  case name, id, gift
}
// 2
enum GiftKeys: CodingKey {
  case toy
}
// 3
func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(name, forKey: .name)
  try container.encode(id, forKey: .id)
  // 4  
  var giftContainer = container
    .nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
  try giftContainer.encode(toy, forKey: .toy)
}

This is how the above code works:

  1. Create your top-level coding keys.
  2. Create another set of coding keys, which you’ll use to create another container.
  3. Encode the name and id the way you’re used to.
  4. Create a nested container nestedContainer(keyedBy:forKey:) and encode toy with it.

Run the playground and inspect the encoded string to see your multi-level JSON. You may use as many nested containers as your JSON has indentation levels. This comes in handy when working with complex and deep JSON hierarchies in real world APIs.

Decoding is straightforward in this case. Add the following extension:

extension Employee: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    id = try container.decode(Int.self, forKey: .id)
    let giftContainer = try container
      .nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
    favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
  }
}

let sameEmployee = try decoder.decode(Employee.self, from: nestedData)

You’ve decoded nestedData to Employee using a nested decoding container .

Encoding and Decoding Dates

The Gifts department needs to know the employees’ birthdays to send out the presents, so their JSON looks like this:

{
  "id" : 7,
  "name" : "John Appleseed",
  "birthday" : "29-05-2019",
  "toy" : {
    "name" : "Teddy Bear"
  }
}

There is no JSON standard for dates, much to the distress of every programmer who’s ever worked with them. JSONEncoder and JSONDecoder will by default use a double representation of the date’s timeIntervalSinceReferenceDate , which is not very common in the wild.

You need to use a date strategy . Add this block of code to Dates , before the try encoder.encode(employee) statement:

// 1
extension DateFormatter {
  static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "dd-MM-yyyy"
    return formatter
  }()
}
// 2
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
decoder.dateDecodingStrategy = .formatted(.dateFormatter)

Here’s what this code does:

  1. Create a date formatter matching your desired format. It’s added as a static property on DateFormatter as this is good practice for your code, so formatters are reusable.
  2. Set dateEncodingStrategy and dateDecodingStrategy to .formatted(.dateFormatter) to tell the encoder and decoder to use the formatter while encoding and decoding dates

Inspect the dateString and check the date format is correct. You’ve made sure the Gifts department will deliver the gifts on time — way to go! :]

Just a few more challenges and you’re done.

Encoding and Decoding Subclasses

The Gifts department API can handle JSON based on class hierarchies :

{
  "toy" : {
    "name" : "Teddy Bear"
  },
  "employee" : {
    "name" : "John Appleseed",
    "id" : 7
  },
  "birthday" : 580794178.33482599
}

employee matches the base class structure which has no toy or birthday . Open Subclasses and make BasicEmployee conform to Codable :

class BasicEmployee: Codable {

This will give you an error, because GiftEmployee is not Codable yet. Correct that by adding the following to GiftEmployee :

// 1              
enum CodingKeys: CodingKey {
  case employee, birthday, toy
}  
// 2
required init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  birthday = try container.decode(Date.self, forKey: .birthday)
  toy = try container.decode(Toy.self, forKey: .toy)
  // 3
  let baseDecoder = try container.superDecoder(forKey: .employee)
  try super.init(from: baseDecoder)
}

This code covers decoding:

  1. Add the relevant coding keys.
  2. Decode the properties specific to the subclass.
  3. Use superDecoder(forKey:) to get a decoder instance suitable to pass to the init(from:) method of the superclass, then initialize the superclass.

Now implement encoding in GiftEmployee :

override func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(birthday, forKey: .birthday)
  try container.encode(toy, forKey: .toy)
  let baseEncoder = container.superEncoder(forKey: .employee)
  try super.encode(to: baseEncoder)
}

It’s the same pattern, but you use superEncoder(forKey:) to prepare the encoder for the superclass. Add the following code to the end of the playground to test out your codable subclass:

let giftEmployee = GiftEmployee(name: "John Appleseed", id: 7, birthday: Date(), 
                                toy: toy)
let giftData = try encoder.encode(giftEmployee)
let giftString = String(data: giftData, encoding: .utf8)!
let sameGiftEmployee = try decoder.decode(GiftEmployee.self, from: giftData)

Inspect the value of giftString to see your work in action! You can handle even more complex class hierarchies in your apps. Time for your next challenge!

Handling Arrays With Mixed Types

The Gifts department API exposes JSON that works with different types of employees:

[
  {
    "name" : "John Appleseed",
    "id" : 7
  },
  {
    "id" : 7,
    "name" : "John Appleseed",
    "birthday" : 580797832.94787002,
    "toy" : {
      "name" : "Teddy Bear"
    }
  }
]

This JSON array is polymorphic because it contains both default and custom employees. Open Polymorphic types and you’ll see that the different types of employee are represented by an enum. First, declare that the enum is Encodable :

enum AnyEmployee: Encodable {

Then add this code to the body of the enum:

// 1
enum CodingKeys: CodingKey {
  case name, id, birthday, toy
}  
// 2
func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  
  switch self {
    case .defaultEmployee(let name, let id):
      try container.encode(name, forKey: .name)
      try container.encode(id, forKey: .id)
    case .customEmployee(let name, let id, let birthday, let toy):  
      try container.encode(name, forKey: .name)
      try container.encode(id, forKey: .id)
      try container.encode(birthday, forKey: .birthday)
      try container.encode(toy, forKey: .toy)
    case .noEmployee:
      let context = EncodingError.Context(codingPath: encoder.codingPath, 
                                          debugDescription: "Invalid employee!")
      throw EncodingError.invalidValue(self, context)
  }
}

Here’s what’s going on with this code:

EncodingError.invalidValue(_:_:)

Test your encoding by adding the following to the end of the playground:

let employees = [AnyEmployee.defaultEmployee("John Appleseed", 7), 
                 AnyEmployee.customEmployee("John Appleseed", 7, Date(), toy)]
let employeesData = try encoder.encode(employees)
let employeesString = String(data: employeesData, encoding: .utf8)!

Inspect the value of employeesString to see your mixed array.

Note : Want to learn more about polymorphism in Swift? Check out the object oriented programming tutorial: Object Oriented Programming in Swift .

Decoding is a little bit more complicated, as you have to work out what is in the JSON before you can decide how to proceed. Add the following code to the playground:

extension AnyEmployee: Decodable {
  init(from decoder: Decoder) throws {
    // 1
    let container = try decoder.container(keyedBy: CodingKeys.self) 
    let containerKeys = Set(container.allKeys)
    let defaultKeys = Set<CodingKeys>([.name, .id])
    let customKeys = Set<CodingKeys>([.name, .id, .birthday, .toy])
   
    // 2
   switch containerKeys {
      case defaultKeys:
        let name = try container.decode(String.self, forKey: .name)
        let id = try container.decode(Int.self, forKey: .id)
        self = .defaultEmployee(name, id)
      case customKeys:
        let name = try container.decode(String.self, forKey: .name)
        let id = try container.decode(Int.self, forKey: .id)
        let birthday = try container.decode(Date.self, forKey: .birthday)
        let toy = try container.decode(Toy.self, forKey: .toy)
        self = .customEmployee(name, id, birthday, toy)
      default:
        self = .noEmployee
    }
  }
}
// 4
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData)

This is how it all works:

  1. Get a keyed container as usual, then inspect the allKeys property to determine which keys were present in the JSON.
  2. Check whether the containerKeys matches the keys needed for a default employee or a custom employee and extract the relevant properties; otherwise, make a .noEmployee . You could choose to throw an error here if there was no suitable default.
  3. Decode employeesData to [AnyEmployee] .

You decode each employee in employeesData based on its concrete type, just as you do for encoding.

Only two more challenges left — time for the next one!

Working With Arrays

The Gifts department adds labels to the employees’ birthday presents; their JSON looks like this:

[
  "teddy bear",
  "TEDDY BEAR",
  "Teddy Bear"
]

The JSON array contains the lower case, upper case and regular label names. You don’t need any keys this time, so you use an unkeyed container .

Open Unkeyed containers and add the encoding code to Label :

func encode(to encoder: Encoder) throws {
  var container = encoder.unkeyedContainer()
  try container.encode(toy.name.lowercased())
  try container.encode(toy.name.uppercased())
  try container.encode(toy.name)
}

An UnkeyedEncodingContainer works just like the containers you’ve been using so far except… you guessed it, there are no keys. Think of it as writing to JSON arrays instead of JSON dictionaries. You encode three different strings to the container.

Run the playground and inspect labelString to see your array.

Here’s how the decoding looks. Add the following code to the end of the playground:

extension Label: Decodable {
  // 1
  init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()
    var name = ""
    while !container.isAtEnd {
      name = try container.decode(String.self)
    }
    toy = Toy(name: name)
  }
}
// 2
let sameLabel = try decoder.decode(Label.self, from: labelData)

This is how the above code works:

  1. Get the decoder’s unkeyed decoding container and loop through it with decode(_:) to decode the final, correctly-formatted label name.
  2. Decode labelData to Label using your unkeyed decoding container.

You loop through the whole decoding container since the correct label name comes at the end.

Time for your last challenge!

Working With Arrays Within Objects

The Gifts department wants to see both the names and the labels of the employees’ birthday gifts, so its API generates JSON that looks like this:

{
  "name" : "Teddy Bear",
  "label" : [
    "teddy bear",
    "TEDDY BEAR",
    "Teddy Bear"
  ]
}

You nest the label names inside label . The JSON structure adds an extra level of indentation compared to the previous challenge, so you need to use nested unkeyed containers for label in this case.

Open Nested unkeyed containers and add the following code to Toy :

func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(name, forKey: .name)
  var labelContainer = container.nestedUnkeyedContainer(forKey: .label)                   
  try labelContainer.encode(name.lowercased())
  try labelContainer.encode(name.uppercased())
  try labelContainer.encode(name)
}

Here you are creating a nested unkeyed container and filling it with the three label values. Run the playground and inspect string to check the structure is right.

You can use more nested containers if your JSON has more indentation levels. Add the decoding code to the playground page:

extension Toy: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    var labelContainer = try container.nestedUnkeyedContainer(forKey: .label)
    var labelName = ""
    while !labelContainer.isAtEnd {
      labelName = try labelContainer.decode(String.self)
    }
    label = labelName
  }
}
let sameToy = try decoder.decode(Toy.self, from: data)

This follows the same pattern as before, working through the array and using the final value to set the value of label , but from a nested unkeyed container.

Congratulations on completing all the challenges! :]

swift-320x320.png

Encoding and decoding in Swift like a pro!

Where to Go From Here?

Download the final playground using the Download Materials button at the top or bottom of the tutorial.

If you want to learn more about encoding and decoding in Swift, check out ourSaving Data in iOS video course. It covers JSON , Property Lists , XML and much more!

I hope you enjoyed this tutorial and if you have any questions or comments, please join the forum discussion below! :]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK