¿Cómo hago una enumeración Decodable en swift 4?

157
enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

    case Image
    enum CodingKeys: String, CodingKey {
        case image
    }
}

¿Qué pongo para completar esto? Además, digamos que cambié caseesto a esto:

case image(value: Int)

¿Cómo hago que esto se ajuste a Decodable?

EDITAR Aquí está mi código completo (que no funciona)

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
}

Edición final Además, ¿cómo manejará una enumeración como esta?

enum PostType: Decodable {
    case count(number: Int)
}
protuberancia rápida
fuente

Respuestas:

262

Es muy fácil, sólo tiene que utilizar Stringo Intvalores en bruto que se asignan de manera implícita.

enum PostType: Int, Codable {
    case image, blob
}

imagees codificado a 0y bloba1

O

enum PostType: String, Codable {
    case image, blob
}

imagees codificado a "image"y bloba"blob"


Este es un ejemplo simple de cómo usarlo:

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)
}
vadian
fuente
1
Probé el código que sugirió, pero no funciona. He editado mi código para mostrar el JSON que estoy tratando de decodificar
swift nub
8
Una enumeración no se puede decodificar / decodificar únicamente. Debe estar incrustado en una estructura. Agregué un ejemplo.
vadian
Marcaré esto como correcto. Pero tenía una última parte en la pregunta anterior que no fue respondida. ¿Qué pasa si mi enumeración se ve así? (editado arriba)
swift nub
Si está utilizando enumeraciones con tipos asociados, debe escribir métodos de codificación y decodificación personalizados. Lea Tipos personalizados de codificación y decodificación
vadian
1
Acerca de "Una enumeración no puede ser decodificada / decodificada únicamente", parece estar resuelta en iOS 13.3. Pruebo iOS 13.3y iOS 12.4.3, se comportan de manera diferente. Debajo iOS 13.3, enum se puede decodificar / decodificar únicamente.
AechoLiu
111

Cómo hacer que las enumeraciones con tipos asociados se ajusten a Codable

Esta respuesta es similar a la de @Howard Lovatt, pero evita crear una PostTypeCodableFormestructura y en su lugar utiliza el KeyedEncodingContainertipo proporcionado por Apple como una propiedad en Encodery Decoder, lo que reduce la repetitiva.

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)
        }
    }
}

Este código funciona para mí en 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)
proxpero
fuente
¡Me encanta esta respuesta! Como nota, este ejemplo también se repite en una publicación en objc.io sobre hacer Eithercodificables
Ben Leggiero
La mejor respuesta
Peter Suwara
38

Swift arrojaría un .dataCorruptederror si encuentra un valor de enumeración desconocido. Si sus datos provienen de un servidor, puede enviarle un valor de enumeración desconocido en cualquier momento (lado del servidor de errores, nuevo tipo agregado en una versión de API y desea que las versiones anteriores de su aplicación manejen el caso con gracia, etc.), será mejor que esté preparado y codifique "estilo defensivo" para decodificar sus enumeraciones de forma segura.

Aquí hay un ejemplo sobre cómo hacerlo, con o sin valor asociado

    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)
          }
       }
    }

Y cómo usarlo en una estructura envolvente:

    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)
      }
   }
Toka
fuente
1
Gracias, su respuesta es mucho más fácil de entender.
DazChong
1
Esta respuesta también me ha ayudado, gracias. Se puede mejorar haciendo que su enumeración herede de String, luego no necesita cambiar las cadenas
Gobe
27

Para ampliar la respuesta de @ Toka, también puede agregar un valor representable sin formato a la enumeración y utilizar el constructor opcional predeterminado para generar la enumeración sin un 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
  }
}

Se puede ampliar utilizando un protocolo personalizado que permite refactorizar el constructor:

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
}

También se puede extender fácilmente para generar un error si se especificó un valor de enumeración no válido, en lugar de omitir un valor. Lo esencial de este cambio está disponible aquí: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128 .
El código fue compilado y probado usando Swift 4.1 / Xcode 9.3.

Stéphane Copin
fuente
1
Esta es la respuesta que vine buscando.
Nathan Hosselton
7

Una variante de la respuesta de @ proxpero que es más terser sería formular el decodificador como:

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)
    }
}

Esto permite que el compilador verifique exhaustivamente los casos, y tampoco suprime el mensaje de error para el caso donde el valor codificado no coincide con el valor esperado de la clave.

marcprux
fuente
Estoy de acuerdo en que esto es mejor.
proxpero
6

En realidad, las respuestas anteriores son realmente geniales, pero les faltan algunos detalles de lo que mucha gente necesita en un proyecto cliente / servidor desarrollado continuamente. Desarrollamos una aplicación mientras nuestro backend evoluciona continuamente con el tiempo, lo que significa que algunos casos de enumeración cambiarán esa evolución. Por lo tanto, necesitamos una estrategia de decodificación de enumeración que sea capaz de decodificar matrices de enumeraciones que contienen casos desconocidos. De lo contrario, la decodificación del objeto que contiene la matriz simplemente falla.

Lo que hice es bastante simple:

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
    }
}

Bonificación: Ocultar implementación> Convertirla en una colección

Ocultar detalles de implementación es siempre una buena idea. Para esto necesitarás un poco más de código. El truco consiste en conformar DirectionsLista Collectiony hacer que su interior listserie privada:

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)
    }
}

Puede leer más sobre la conformidad con las colecciones personalizadas en esta publicación de blog de John Sundell: https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb0

blackjacx
fuente
5

Puedes hacer lo que quieras, pero es un poco complicado :(

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)
Howard Lovatt
fuente
truco interesante
Roman Filippov