Como faço para enum decodificável no swift 4?

157
enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

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

O que eu coloco para concluir isso? Além disso, digamos que eu mudei o casepara isso:

case image(value: Int)

Como faço para que isso esteja em conformidade com Decodable?

Editar Aqui está o meu código completo (que não 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
}

Edição final Além disso, como ele lida com uma enumeração como esta?

enum PostType: Decodable {
    case count(number: Int)
}
ponto rápido
fonte

Respostas:

262

É muito fácil, basta usar Stringou Intvalores brutos atribuídos implicitamente.

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

imageé codificado para 0e blobpara1

Ou

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

imageé codificado para "image"e blobpara"blob"


Este é um exemplo simples de como usá-lo:

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
fonte
1
Eu tentei o código que você sugeriu, mas não funciona. Eu editei meu código para mostrar o JSON que estou tentando decodificar
swift nub
8
Um enum não pode ser decodificado unicamente. Ele deve ser incorporado em uma estrutura. Eu adicionei um exemplo.
Vadian
Vou sinalizar isso como correto. Mas teve uma última parte na pergunta acima que não foi respondida. E se meu enum fosse assim? (editada acima)
swift nub
Se você estiver usando enumerações com tipos associados, precisará escrever métodos personalizados de codificação e decodificação. Leia Tipos personalizados de codificação e decodificação
vadian
1
Sobre "Um enum não pode ser decodificado unicamente.", Parece ter sido resolvido em iOS 13.3. Eu testei iOS 13.3e iOS 12.4.3eles se comportam de maneira diferente. Sob iOS 13.3, enum pode ser en- / decodificado apenas.
AechoLiu 17/01
111

Como fazer com que as enums com tipos associados estejam em conformidade com Codable

Esta resposta é semelhante à de HowHow Lovatt, mas evita a criação de uma PostTypeCodableFormestrutura e, em vez disso, usa o KeyedEncodingContainertipo fornecido pela Apple como uma propriedade em Encodere Decoder, o que reduz o padrão.

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 mim no 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
fonte
Eu amo essa resposta! Como uma nota, este exemplo também é ecoado em um post no objc.io sobre fazer Eithercodificável
Ben leggiero
A melhor resposta
Peter Suwara 13/03/19
38

Swift lançaria um .dataCorruptederro se encontrar um valor de enumeração desconhecido. Se seus dados vierem de um servidor, eles poderão enviar um valor de enumeração desconhecido a qualquer momento (erro no servidor, novo tipo adicionado em uma versão da API e você deseja que as versões anteriores do seu aplicativo lidem com o caso normalmente, etc.), é melhor você estar preparado e codificar "estilo defensivo" para decodificar com segurança seus enums.

Aqui está um exemplo de como fazê-lo, com ou sem valor associado

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

E como usá-lo em uma estrutura anexa:

    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
fonte
1
Obrigado, sua resposta é muito mais fácil de entender.
DazChong 26/07/2018
1
Esta resposta também me ajudou, obrigado. Ele pode ser melhorada, tornando a sua enumeração herdam String, então você não precisa passar cordas
Gobe
27

Para estender a resposta de @ Toka, você também pode adicionar um valor representável bruto à enumeração e usar o construtor opcional padrão para criar a enumeração sem 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
  }
}

Pode ser estendido usando um protocolo personalizado que permite refatorar o construtor:

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
}

Também pode ser facilmente estendido para gerar um erro se um valor de enum inválido foi especificado, em vez de usar como padrão um valor. A lista principal com esta alteração está disponível aqui: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128 .
O código foi compilado e testado usando o Swift 4.1 / Xcode 9.3.

Stéphane Copin
fonte
1
Esta é a resposta que eu vim procurando.
Nathan Hosselton
7

Uma variante da resposta do @ proxpero que é terser seria formular o 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)
    }
}

Isso permite que o compilador verifique exaustivamente os casos e também não suprime a mensagem de erro para o caso em que o valor codificado não corresponde ao valor esperado da chave.

marcprux
fonte
Eu concordo que isso é melhor.
#
6

Na verdade, as respostas acima são realmente ótimas, mas faltam alguns detalhes para o que muitas pessoas precisam em um projeto cliente / servidor desenvolvido continuamente. Desenvolvemos um aplicativo enquanto nosso back-end evolui continuamente ao longo do tempo, o que significa que alguns casos de enum mudarão essa evolução. Portanto, precisamos de uma estratégia de decodificação de enumeração capaz de decodificar matrizes de enumerações que contêm casos desconhecidos. Caso contrário, a decodificação do objeto que contém a matriz simplesmente falha.

O que eu fiz é bem simples:

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

Bônus: ocultar implementação> Criar uma coleção

Ocultar os detalhes da implementação é sempre uma boa ideia. Para isso, você precisará de um pouco mais de código. O truque consiste em conformar DirectionsLista Collectione faça a sua interna listvariedade 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)
    }
}

Você pode ler mais sobre a conformidade com coleções personalizadas nesta postagem de blog de John Sundell: https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb0

blackjacx
fonte
5

Você pode fazer o que quiser, mas está um pouco envolvido :(

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
fonte
hack interessante
Roman Filippov