Come faccio a rendere enum Decodable in swift 4?


157
enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

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

Cosa metto per completare questo? Inoltre, diciamo che ho cambiato il casein questo:

case image(value: Int)

Come posso renderlo conforme a Decodable?

Modifica Ecco il mio codice completo (che non funziona)

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
}

Modifica finale Inoltre, come gestirà un enum come questo?

enum PostType: Decodable {
    case count(number: Int)
}

Risposte:


262

È abbastanza facile, basta usare Stringo Intvalori grezzi che sono assegnati in modo implicito.

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

imageè codificato da 0e blobverso1

O

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

imageè codificato da "image"e blobverso"blob"


Questo è un semplice esempio su come 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)
}

1
ho provato il codice che hai suggerito, ma non funziona. Ho modificato il mio codice per mostrare il JSON che sto cercando di decodificare
swift nub il

8
Un enum non può essere codificato / decodificato esclusivamente. Deve essere incorporato in una struttura. Ho aggiunto un esempio.
vadian

Lo contrassegnerò come corretto. Ma aveva un'ultima parte della domanda sopra a cui non è stata data risposta. E se il mio enum fosse così? (modificato sopra)
swift nub

Se si utilizzano enum con tipi associati, è necessario scrivere metodi di codifica e decodifica personalizzati. Leggere i tipi personalizzati di codifica e decodifica
vadian

1
A proposito di "Un enum non può essere codificato / decodificato unicamente", sembra risolto iOS 13.3. Ci provo iOS 13.3e iOS 12.4.3, si comportano diversamente. Sotto iOS 13.3, enum può essere en- / decodificato esclusivamente.
AechoLiu

111

Come rendere conformi gli enum con i tipi associati Codable

Questa risposta è simile a quella di @Howard Lovatt, ma evita di creare una PostTypeCodableFormstruttura e utilizza invece il KeyedEncodingContainertipo fornito da Apple come proprietà su Encodere Decoder, il che riduce la piastra di cottura .

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

Questo codice funziona per me su 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)

Adoro questa risposta! Come nota, questo esempio è anche ripreso in un post su objc.io sul rendere Eithercodificabile
Ben Leggiero,

La migliore risposta
Peter Suwara,

38

Swift genererebbe un .dataCorruptederrore se incontra un valore enum sconosciuto. Se i tuoi dati provengono da un server, possono inviarti un valore enum sconosciuto in qualsiasi momento (lato server bug, nuovo tipo aggiunto in una versione API e desideri che le versioni precedenti dell'app gestiscano il caso con grazia, ecc.), faresti meglio a essere preparato, e codificare "stile difensivo" per decodificare in sicurezza i tuoi enum.

Ecco un esempio su come farlo, con o senza valore associato

    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 come usarlo in una struttura chiusa:

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

1
Grazie, la tua risposta è molto più semplice da capire.
DazChong,

1
Anche questa risposta mi ha aiutato, grazie. Può essere migliorato ereditando l'enum da String, quindi non è necessario cambiare le stringhe
Gobe

27

Per estendere la risposta di @ Toka, potresti anche aggiungere un valore rappresentabile non elaborato all'enum e utilizzare il costruttore opzionale predefinito per creare l'enum senza 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
  }
}

Può essere esteso utilizzando un protocollo personalizzato che consente di refactoring il costruttore:

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
}

Può anche essere facilmente esteso per generare un errore se è stato specificato un valore enum non valido, piuttosto che per impostazione predefinita su un valore. L'essenziale con questa modifica è disponibile qui: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128 .
Il codice è stato compilato e testato utilizzando Swift 4.1 / Xcode 9.3.


1
Questa è la risposta che ho cercato.
Nathan Hosselton,

7

Una variante della risposta di @ proxpero che è più terser sarebbe quella di formulare il decoder come:

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

Ciò consente al compilatore di verificare in modo esaustivo i casi e inoltre non sopprime il messaggio di errore nel caso in cui il valore codificato non corrisponda al valore previsto della chiave.


Sono d'accordo che sia meglio.
Proxpero

6

In realtà le risposte sopra sono davvero fantastiche, ma mancano alcuni dettagli per ciò di cui molte persone hanno bisogno in un progetto client / server continuamente sviluppato. Sviluppiamo un'app mentre il nostro backend si evolve continuamente nel tempo, il che significa che alcuni casi di enum cambieranno tale evoluzione. Quindi abbiamo bisogno di una strategia di decodifica dell'enum che sia in grado di decodificare matrici di enum che contengono casi sconosciuti. In caso contrario, la decodifica dell'oggetto che contiene l'array non riesce.

Quello che ho fatto è abbastanza semplice:

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

Bonus: nascondi implementazione> Rendi una raccolta

Nascondere i dettagli di implementazione è sempre una buona idea. Per questo avrai bisogno di un po 'più di codice. Il trucco è quello di conformarsi DirectionsLista Collectione rendere il vostro interno listdi matrice privata:

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

Puoi leggere di più sulla conformità alle raccolte personalizzate in questo post di blog di John Sundell: https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb0


5

Puoi fare quello che vuoi, ma è un po 'coinvolto :(

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)

hack interessante
Roman Filippov,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.