Я пытаюсь принять протокол Codable
для объекта, который должен быть создан из JSON, который мой веб-сервис возвращает в ответ на один из вызовов API.
Одно из свойств имеет тип перечисление и является необязательным: nil
означает, что не выбран ни один из параметров, определенных enum
.
Константы enum
основаны на Int
и начинаются с 1
, , а не 0
:
class MyClass: Codable {
enum Company: Int {
case toyota = 1
case ford
case gm
}
var company: Company?
Это потому, что значение 0
в соответствующей записи JSON зарезервировано для «не установлено»; т.е. он должен быть сопоставлен с nil
при настройке инициализации свойства company
из него.
Инициализатор перечисления Swift init?(rawValue:)
предоставляет эту функциональность "из коробки": аргумент Int
, который не соответствует необработанному значению любого случая, приведет к сбою инициализатора и возврату nil. Кроме того, перечисления на основе Int
(и String) можно сделать так, чтобы они соответствовали Codable
, просто объявив его в определении типа:
enum Company: Int, Codable {
case toyota = 1
case ford
case gm
}
Проблема в том, что мой пользовательский класс имеет более 20 свойств, поэтому я действительно очень хочу избежать реализации init(from:)
и encode(to:)
, полагаясь вместо этого на автоматическое поведение, полученное путем предоставления CondingKeys
пользовательский тип перечисления.
Это приводит к сбою инициализации всего экземпляра класса, поскольку кажется, что «синтезированный» инициализатор не может сделать вывод, что неподдерживаемое необработанное значение типа enum должно рассматриваться как nil
(даже если цель свойство четко помечено как необязательно , т.е. Company?
).
Я думаю, что это так, потому что инициализатор, предоставленный Decodable
, может выдать, но не может вернуть nil:
// This is what we have:
init(from decoder: Decoder) throws
// This is what I would want:
init?(from decoder: Decoder)
В качестве обходного пути я реализовал свой класс следующим образом: отобразить целочисленное свойство JSON в private , хранимое Int
свойство моего класса, которое служит только для хранения, и ввести строго типизированное вычисляемое свойство, которое действует как мост между хранилищем и остальной частью моего приложения:
class MyClass {
// (enum definition skipped, see above)
private var companyRawValue: Int = 0
public var company: Company? {
set {
self.companyRawValue = newValue?.rawValue ?? 0
// (sets to 0 if passed nil)
}
get {
return Company(rawValue: companyRawValue)
// (returns nil if raw value is 0)
}
}
enum CodingKeys: String, CodingKey {
case companyRawValue = "company"
}
// etc...
Мой вопрос: есть ли лучший (более простой / более элегантный) способ , который:
- Не требует ли повторяющиеся свойства, такие как мой обходной путь, и
- Не требует ли * полной реализации
init(from:)
и / или encode(with:)
, возможно, реализуя их упрощенные версии, которые по большей части делегируют поведение по умолчанию (т. Е. Не требуют всей совокупности вручную инициализация / кодирование каждого свойства)?
Приложение: Существует третье, также не элегантное решение, которое мне не пришло в голову, когда я впервые опубликовал вопрос. Это предполагает использование искусственного базового класса только для автоматического декодирования. Я не буду его использовать, а просто опишу здесь для полноты:
// Groups all straight-forward decodable properties
//
class BaseClass: Codable {
/*
(Properties go here)
*/
enum CodingKeys: String, CodingKey {
/*
(Coding keys for above properties go here)
*/
}
// (init(from decoder: Decoder) and encode(to:) are
// automatically provided by Swift)
}
// Actually used by the app
//
class MyClass: BaseClass {
enum CodingKeys: String, CodingKey {
case company
}
var company: Company? = nil
override init(from decoder: Decoder) throws {
super.init(from: decoder)
let values = try decoder.container(keyedBy: CodingKeys.self)
if let company = try? values.decode(Company.self, forKey: .company) {
self.company = company
}
}
}
... Но это действительно безобразный хак. Иерархия наследования классов не должна диктоваться недостатками этого типа.