Всё о Codable в Swift 4

27 февраля 2018

Забудьте о нагромождениях кода времён NSCoding..!!!

Как все мы знаем, чтобы обеспечить поддержку кодирования и декодирования экземпляров в iOS, класс должен поддерживать протокол NSCoding и реализовывать его методы:

    init(coder:) — Возвращает объект, инициализированный на основе данных заданных разархиватором.

    encode(with:) — Кодирует принимающую сторону, используя заданный архиватор.

Пример:

class City: NSObject, NSCoding
{
    var name: String?
    var id: Int?
    
    required init?(coder aDecoder: NSCoder)
    {
        self.name = aDecoder.decodeObject(forKey: "name") as? String
        self.id = aDecoder.decodeObject(forKey: "id") as? Int
    }
    
    func encode(with aCoder: NSCoder)
    {
        aCoder.encode(self.name, forKey: "name")
        aCoder.encode(self.id, forKey: "id")
    }
}

init(coder:) и encode(with:) должны содержать код для каждого свойства, которое необходимо кодировать или декодировать.

Тут, похоже, придётся писать много дублирующегося кода, меняя только имя свойства. Копировать Вставить.. Копировать Вставить..!!

Но почему я должна это делать?
♀ Если имена свойств совпадают с именами ключей и нет никаких специфических требований, почему NSCoding не решает это собственными силами?
♀ Почему нужно писать так много кода? Неееееет..!!!

ОМГ..!! Ещё одна проблема!
♀. Нет даже поддержки struct’ов и enum’ов. А это значит, что я должна создавать класс всякий раз, когда нужно сериализировать данные, даже если  на уровне класса нет никаких специфических требований.

Сплошная потеря времени. Спасите..!!!

Так..так..так..Не переживайте. Apple снова приходит на помощь.

Что изменилось в Swift 4?

Apple в релизе Swift 4 представила совершенно новый способ кодирования и декодирования данных посредством приведения ваших пользовательских типов в соответствие с некоторыми простыми в использовании протоколами:

  1. Encodable — для кодирования
  2. Decodable — для декодирования
  3. Codable — для кодирования и декодирования вместе

С поддержкой классов, структур и enum’ов.

Итак, давайте разберёмся, чем это для нас полезно.

Протокол Encodable

Тип, способный кодировать себя во внешнюю репрезентацию. Используется для типов, которые могут быть закодированы.

Содержит единственный метод:

encode(to:) — Кодирует значение в заданный кодировщик.

Протокол Decodable

Тип, который может декодировать себя из внешней репрезентации. Используется для типов, которые могут быть декодированы.

Также содержит всего один метод:

init(from:) — Создаёт новый экземпляр класса, декодируя из заданного декодировщика.

Протокол Сodable

Тип, который может преобразовывать себя в/из внешней репрезентации. Используется типами, которые могут быть кодированы и декодированы.

typealias Codable = Decodable & Encodable

Содержит методы, объявленные в Encodable и Decodable.

Из разделов ниже вы узнаете больше об encode(to:) и init(from:)

Codable тип

Чтобы кодировать и декодировать пользовательский тип, мы должны сделать его Codable.

Самый простой способ сделать тип codable — это объявить его свойства с использованием типов данных, уже являющихся Codable.

  1. Встроенные Codable типы:  String, Int, Double, Data, URL, Bool
  2. Массивы, словари, опционалы являются Codable, если содержат Codable типы
struct Photo: Codable {
    //String, URL, Bool and Date conform to Codable.
    var title: String
    var url: URL
    var isSample: Bool
    
    //The Dictionary is of type [String:String] and String already conforms to Codable.
    var metaData: [String:String]
    
    //PhotoType and Size are also Codable types
    var type: PhotoType
    var size: Size
}

struct Size: Codable {
    var height: Double
    var width: Double
}

enum PhotoType: String, Codable {
    case flower
    case animal
    case fruit
    case vegetable
}

Итак, теперь, когда мы увидели, что Codable из себя представляет, давайте посмотрим, как, собственно, его использовать для кодирования и декодирования своих пользовательских типов. Поехали!.

Кодирование — JSONEncoder

JSONEncoder можно использовать для преобразования codable типа в Data.

Метод encode(_:) JSONEncoder’а возвращает кодированную в JSON-репрезентацию codable типа.

let photoObject = Photo(title: "Hibiscus", url: URL(string: "https://www.flowers.com/hibiscus")!, isSample: false, metaData: ["color" : "red"], type: .flower, size: Size(width: 200, height: 200))
let encodedData = try? JSONEncoder().encode(photoObject)

Чтоооооо…!!! Всего одна строка кода? И всё? Шутите? Наверняка есть какой-то подвох.. не может такого быть...
Да, это всё, что вы должны написать, чтобы оно заработало. Круто, неправда ли?

Всего два простых шага, и готово. Никакого больше громоздкого избыточного кода. Дайте пять ✋.

Декодирование освоить так же легко, как и кодирование. Чего же мы ждём? Продолжим...

Декодирование — JSONDecoder

Аналогично JSONEncoder’у, существует и JSONDecoder, который можно использовать для декодирования данных формата JSON обратно в ваш codable тип.

Метод JSONDecoder’а decode(_:from:) возвращает значение указанного вами codable типа, декодированное из JSON-объекта.

let jsonString = """ {
    "type":"fruit",
    "size":{
               "width":150,
               "height":150
           },
    "title":"Apple",
    "url":"https:\\/\\/www.fruits.com\\/apple",
    "isSample":true,
    "metaData":{
                  "color":"green"
               }
}
"""
if let jsonData = jsonString.data(using: .utf8){
    let photoObject = try? JSONDecoder().decode(Photo.self, from: jsonData)
}

Вот и всё. Так вы можете кодировать/декодировать свой Codable тип. Всего два шага:

  1. Приведите свой пользовательский тип в соответствие с протоколом Codable.
  2. Используйте JSONEncoder/JSONDecoder для кодирования/декодирования вашего объекта.

Отбор свойств для Encode и Decode:  CodingKeys

Возможно у вас возникли некоторые вопросы ❓❓

  1. Что если я хочу исключить некоторые Codable типы из процедуры сериализации?
  2. Как выполнять кодирование/декодирование, если некоторые ключи сериализованной формы не соответствуют именам свойств Codable типа?

Ну, Apple предоставила решение и на этот случай:  enum CodingKeys.

Codable типы могут объявлять специальное вложенное перечисление (enum) с именем CodingKeys, соответствующее протоколу CodingKey. Если присутствует такое перечисление, его кейсы служат официальным списком свойств, которые должны учитываться при кодировании/декодировании codable типа.

Важные замечания о CodingKeys:

  1. Он соответствует протоколу CodingKey и его исходные (raw) значения имеют тип  String .
  2. Имена кейсов enum’а должны в точности совпадать с именами свойств Codable типа.
  3. Ответ на вопрос №1 ✅— Не включайте свойства в CodingKeys, если хотите исключить их из процесса кодирования/декодирования. Свойство, исключённое из CodingKeys, должно иметь значение по умолчанию.
  4. Ответ на вопрос №2 ✅— Исходное значение понадобится вам, если имена свойств Codable типа не будут совпадать с ключами в сериализованных данных. Предусмотрите альтернативные ключи, используя исходные значения в формате String для перечисления CodingKeys. Строка, которую вы используете как исходное значение для каждого кейса  enum’а - это имя ключа, используемого при кодировании и декодировании.

Пример:

В приведённом ниже фрагменте кода

  1. var format: String = “png” не включён в CodingKeys и поэтому имеет дефолтное значение.
  2. Свойства “title” и “url” переименованы в “name” и “link” посредством исходных значений CodingKeys:  case title = “name” и case url = “link”
struct Photo: Codable {
    //...Other properties (described in Code Snippet - 1)...
    
    //This property is not included in the CodingKeys enum and hence will not be encoded/decoded.
    var format: String = "png"
    
    enum CodingKeys: String, CodingKey {
        case title = "name"
        case url = "link"
        case isSample
        case metaData
        case type
        case size
    }
}

Encode и Decode вручную ✏

Существуют сценарии, при которых структура вашего Codable типа меняется в зависимости от его кодированной формы, например:

struct Photo: Codable {
    var title: String
    var size: Size
    
    enum CodingKeys: String, CodingKey {
        case title = "name"
        case size
    }
}

struct Size: Codable {
    var width: Double
    var height: Double
}

Когда вы кодируете Photo объект, используя приведённое выше объявление Photo, JSON, который вы получите, будет выглядеть примерно так:

{
    "size":{
               "width":150,
               "height":150
           },
    "name":"Apple"
}

Но что если вы не хотите использовать “width” и “height”, вложенные в “size”? т.е. ваш JSON должен выглядеть как-то так:

{
    "title":"Apple",
    "width":150,
    "height":150
}

В этом случае вы можете реализовать свою собственную произвольную логику Encodable и Decodable, чтобы определить собственную логику кодирования и декодирования.

Вам необходимо в явной форме реализовать методы encode(to:) и init(from:) протоколов Encodable и Decodable.

Следуйте приведённым ниже шагам:

  1. Измените CodingKeys enum’а, чтобы включить width и height вместо size.
  2. Устраните подпись Photo под протокол Codable.
  3. Создайте extension для Photo с подпиской под протокол Encodable и реализуйте метод encode(to:).
  4. Создайте extension для Photo с подпиской под протокол Decodable и реализуйте метод init(from:).
struct Photo {
    var title: String
    var size: Size
    
    enum CodingKeys: String, CodingKey {
        case title = "name"
        case width
        case height
    }
}

extension Photo: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encode(size.width, forKey: .width)
        try container.encode(size.height, forKey: .height)
    }
}

extension Photo: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        title = try values.decode(String.self, forKey: .title)
        let width = try values.decode(Double.self, forKey: .width)
        let height = try values.decode(Double.self, forKey: .height)
        size = Size(width: width, height: height)
    }
}

Ограничения

1. Пока что вы не можете подписываться под протокол Codable в extension’е
2. Возможно, это станет доступным в будущих релизах.

Очень много “пока что” в текущем релизе. Не знаю, почему Apple так спешат...

2. Вы должны использовать конкретный тип для кодирования и декодирования.

Хороший пример использования этого сценария вы можете найти здесь.

 

Автор: Payal Gupta

Оригинал статьи.

Перевод: Борис Радченко, radchenko.boris@gmail.com

Содержание