Как сделать enum Decodable в swift 4?
enum PostType: Decodable {
init(from decoder: Decoder) throws {
// What do i put here?
}
case Image
enum CodingKeys: String, CodingKey {
case image
}
}
Что я могу сделать для этого? Кроме того, скажем, я изменил case
на это:
case image(value: Int)
Как я могу сделать это совместимым с Decodable?
EDit Вот мой полный код (который не работает)
let jsonData = """
{
"count": 4
}
""".data(using: .utf8)!
do {
let decoder = JSONDecoder()
let response = try decoder.decode(PostType.self, from: jsonData)
print(response)
} catch {
print(error)
}
}
}
enum PostType: Int, Codable {
case count = 4
}
Final Edit Также, как он будет обрабатывать перечисление, подобное этому?
enum PostType: Decodable {
case count(number: Int)
}
Ответы
Ответ 1
Это довольно просто, просто используйте значения String
или Int
raw, которые неявно назначены.
enum PostType: Int, Codable {
case image, blob
}
image
закодировано до 0
и blob
до 1
Или же
enum PostType: String, Codable {
case image, blob
}
image
закодировано в "image"
и blob
для "blob"
Это простой пример того, как его использовать:
enum PostType : Int, Codable {
case count = 4
}
struct Post : Codable {
var type : PostType
}
let jsonString = "{\"type\": 4}"
let jsonData = Data(jsonString.utf8)
do {
let decoded = try JSONDecoder().decode(Post.self, from: jsonData)
print("decoded:", decoded.type)
} catch {
print(error)
}
Ответ 2
Как привести перечисления с соответствующими типами в соответствие с Codable
Этот ответ похож на @Howard Lovatt, но избегает создания структуры PostTypeCodableForm
и вместо этого использует тип KeyedEncodingContainer
предоставленный Apple, в качестве свойства в Encoder
и Decoder
, что сокращает количество шаблонов.
enum PostType: Codable {
case count(number: Int)
case title(String)
}
extension PostType {
private enum CodingKeys: String, CodingKey {
case count
case title
}
enum PostTypeCodingError: Error {
case decoding(String)
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? values.decode(Int.self, forKey: .count) {
self = .count(number: value)
return
}
if let value = try? values.decode(String.self, forKey: .title) {
self = .title(value)
return
}
throw PostTypeCodingError.decoding("Whoops! \(dump(values))")
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .count(let number):
try container.encode(number, forKey: .count)
case .title(let value):
try container.encode(value, forKey: .title)
}
}
}
Этот код работает для меня на Xcode 9b3.
import Foundation // Needed for JSONEncoder/JSONDecoder
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let decoder = JSONDecoder()
let count = PostType.count(number: 42)
let countData = try encoder.encode(count)
let countJSON = String.init(data: countData, encoding: .utf8)!
print(countJSON)
// {
// "count" : 42
// }
let decodedCount = try decoder.decode(PostType.self, from: countData)
let title = PostType.title("Hello, World!")
let titleData = try encoder.encode(title)
let titleJSON = String.init(data: titleData, encoding: .utf8)!
print(titleJSON)
// {
// "title": "Hello, World!"
// }
let decodedTitle = try decoder.decode(PostType.self, from: titleData)
Ответ 3
Swift выбрасывает ошибку .dataCorrupted
если встречает неизвестное значение enum. Если ваши данные поступают с сервера, он может отправить вам неизвестное значение перечисления в любое время (сторона сервера ошибок, новый тип, добавленный в версию API, и вы хотите, чтобы предыдущие версии вашего приложения обрабатывали случай изящно и т.д.), вам лучше подготовиться и ввести код "защитный стиль", чтобы безопасно расшифровать ваши перечисления.
Вот пример того, как это сделать, с или без связанного значения
enum MediaType: Decodable {
case audio
case multipleChoice
case other
// case other(String) -> we could also parametrise the enum like that
init(from decoder: Decoder) throws {
let label = try decoder.singleValueContainer().decode(String.self)
switch label {
case "AUDIO": self = .audio
case "MULTIPLE_CHOICES": self = .multipleChoice
default: self = .other
// default: self = .other(label)
}
}
}
И как использовать его в закрывающей структуре:
struct Question {
[...]
let type: MediaType
enum CodingKeys: String, CodingKey {
[...]
case type = "type"
}
extension Question: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
[...]
type = try container.decode(MediaType.self, forKey: .type)
}
}
Ответ 4
Чтобы расширить ответ @Toka, вы также можете добавить необработанное представимое значение в перечисление и использовать необязательный конструктор по умолчанию для создания перечисления без switch
:
enum MediaType: String, Decodable {
case audio = "AUDIO"
case multipleChoice = "MULTIPLE_CHOICES"
case other
init(from decoder: Decoder) throws {
let label = try decoder.singleValueContainer().decode(String.self)
self = MediaType(rawValue: label) ?? .other
}
}
Он может быть расширен с помощью пользовательского протокола, который позволяет реорганизовать конструктор:
protocol EnumDecodable: RawRepresentable, Decodable {
static var defaultDecoderValue: Self { get }
}
extension EnumDecodable where RawValue: Decodable {
init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer().decode(RawValue.self)
self = Self(rawValue: value) ?? Self.defaultDecoderValue
}
}
enum MediaType: String, EnumDecodable {
static let defaultDecoderValue: MediaType = .other
case audio = "AUDIO"
case multipleChoices = "MULTIPLE_CHOICES"
case other
}
Это также может быть легко расширено для выдачи ошибки, если было указано недопустимое значение перечисления, вместо значения по умолчанию для значения. С этим изменением можно ознакомиться здесь: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128.
Код был скомпилирован и протестирован с использованием Swift 4.1/Xcode 9.3.
Ответ 5
Вариантом ответа @proxpero, который является terser, было бы сформулировать декодер как:
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
guard let key = values.allKeys.first else { throw err("No valid keys in: \(values)") }
func dec<T: Decodable>() throws -> T { return try values.decode(T.self, forKey: key) }
switch key {
case .count: self = try .count(dec())
case .title: self = try .title(dec())
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .count(let x): try container.encode(x, forKey: .count)
case .title(let x): try container.encode(x, forKey: .title)
}
}
Это позволяет компилятору исчерпывающе проверять случаи, а также не подавляет сообщение об ошибке для случая, когда закодированное значение не соответствует ожидаемому значению ключа.
Ответ 6
Вы можете делать то, что хотите, но это немного связано :(
import Foundation
enum PostType: Codable {
case count(number: Int)
case comment(text: String)
init(from decoder: Decoder) throws {
self = try PostTypeCodableForm(from: decoder).enumForm()
}
func encode(to encoder: Encoder) throws {
try PostTypeCodableForm(self).encode(to: encoder)
}
}
struct PostTypeCodableForm: Codable {
// All fields must be optional!
var countNumber: Int?
var commentText: String?
init(_ enumForm: PostType) {
switch enumForm {
case .count(let number):
countNumber = number
case .comment(let text):
commentText = text
}
}
func enumForm() throws -> PostType {
if let number = countNumber {
guard commentText == nil else {
throw DecodeError.moreThanOneEnumCase
}
return .count(number: number)
}
if let text = commentText {
guard countNumber == nil else {
throw DecodeError.moreThanOneEnumCase
}
return .comment(text: text)
}
throw DecodeError.noRecognizedContent
}
enum DecodeError: Error {
case noRecognizedContent
case moreThanOneEnumCase
}
}
let test = PostType.count(number: 3)
let data = try JSONEncoder().encode(test)
let string = String(data: data, encoding: .utf8)!
print(string) // {"countNumber":3}
let result = try JSONDecoder().decode(PostType.self, from: data)
print(result) // count(3)
Ответ 7
На самом деле ответы выше действительно хороши, но в них отсутствуют некоторые детали того, что нужно многим людям в постоянно развивающемся проекте клиент/сервер. Мы разрабатываем приложение, в то время как наш бэкэнд постоянно развивается с течением времени, что означает, что некоторые перечисленные случаи изменят эту эволюцию. Поэтому нам нужна стратегия декодирования перечислений, которая способна декодировать массивы перечислений, которые содержат неизвестные регистры. В противном случае декодирование объекта, содержащего массив, просто не удастся.
То, что я сделал, довольно просто:
enum Direction: String, Decodable {
case north, south, east, west
}
struct DirectionList {
let directions: [Direction]
}
extension DirectionList: Decodable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var directions: [Direction] = []
while !container.isAtEnd {
// Here we just decode the string from the JSON which always works as long as the array element is a string
let rawValue = try container.decode(String.self)
guard let direction = Direction(rawValue: rawValue) else {
// Unknown enum value found - ignore, print error to console or log error to analytics service so you'll always know that there are apps out which cannot decode enum cases!
continue
}
// Add all known enum cases to the list of directions
directions.append(direction)
}
self.directions = directions
}
}
Бонус: Скрыть реализацию> Сделать коллекцию
Скрывать детали реализации всегда хорошая идея. Для этого вам понадобится немного больше кода. Хитрость заключается в том, чтобы согласовать DirectionsList
с Collection
и сделать внутренний массив list
закрытым:
struct DirectionList {
typealias ArrayType = [Direction]
private let directions: ArrayType
}
extension DirectionList: Collection {
typealias Index = ArrayType.Index
typealias Element = ArrayType.Element
// The upper and lower bounds of the collection, used in iterations
var startIndex: Index { return directions.startIndex }
var endIndex: Index { return directions.endIndex }
// Required subscript, based on a dictionary index
subscript(index: Index) -> Element {
get { return directions[index] }
}
// Method that returns the next index when iterating
func index(after i: Index) -> Index {
return directions.index(after: i)
}
}
Вы можете узнать больше о соответствии пользовательских коллекций в этом сообщении в блоге Джона Сунделла: https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb0