web-dev-qa-db-fra.com

Comment créer une énumération Decodable dans Swift 4?

enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

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

Qu'est-ce que je mets pour compléter cela? Aussi, disons que j'ai changé le case en ceci:

case image(value: Int)

Comment puis-je rendre cela conforme à Decodable?

EDit Voici mon code complet (qui ne fonctionne pas)

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 Aussi, comment va-t-il gérer une telle énumération?

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

C'est assez facile, utilisez simplement String ou Int valeurs brutes qui sont assignées de manière implicite.

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

image est codé en 0 et blob en 1

Ou

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

image est codé en "image" et blob en "blob"


Voici un exemple simple d'utilisation:

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

Comment rendre les énumérations avec les types associés conformes à Codable

Cette réponse est similaire à celle de @Howard Lovatt mais évite de créer une structure PostTypeCodableForm et utilise à la place le type KeyedEncodingContainerfourni par Apple comme une propriété sur Encoder et Decoder, ce qui réduit le passe-partout.

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

Ce code fonctionne pour moi sur 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)
72
proxpero

Swift génère une erreur .dataCorrupted s'il rencontre une valeur enum inconnue. Si vos données proviennent d'un serveur, il peut vous envoyer à tout moment une valeur enum inconnue (côté serveur de bogues, nouveau type ajouté dans une version de l'API et vous souhaitez que les versions précédentes de votre application traitent correctement le cas, etc.), vous feriez mieux de vous préparer et de coder "style défensif" pour décoder en toute sécurité vos enums.

Voici un exemple sur la façon de le faire, avec ou sans valeur associée

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

Et comment l'utiliser dans une structure englobante:

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

Pour étendre la réponse de @ Toka, vous pouvez également ajouter une valeur représentable brute à l'énum, ​​et utiliser le constructeur facultatif par défaut pour construire l'énum sans 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
  }
}

Il peut être étendu en utilisant un protocole personnalisé qui permet de refactoriser le constructeur:

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
}

Il peut également être facilement étendu pour générer une erreur si une valeur enum non valide a été spécifiée, plutôt que par défaut sur une valeur. La modification avec cette modification est disponible ici: https://Gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128 .
Le code a été compilé et testé avec Swift 4.1/Xcode 9.3.

13
Stéphane Copin

Une variante de la réponse de @ proxpero, appelée testeur, serait de formuler le décodeur de la manière suivante:

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

Cela permet au compilateur de vérifier de manière exhaustive les cas et ne supprime pas le message d'erreur dans le cas où la valeur codée ne correspond pas à la valeur attendue de la clé.

5
marcprux

Vous pouvez faire ce que vous voulez, mais c'est un peu compliqué :(

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)
3
Howard Lovatt

En réalité, les réponses ci-dessus sont vraiment excellentes, mais il manque quelques détails pour ce dont beaucoup de personnes ont besoin dans un projet client/serveur développé en permanence. Nous développons une application alors que notre système évolue continuellement avec le temps, ce qui signifie que certains cas énormes vont changer cette évolution. Nous avons donc besoin d’une stratégie de décodage d’énumération capable de décoder des tableaux d’énumérations contenant des cas inconnus. Sinon, le décodage de l'objet contenant le tableau échoue simplement.

Ce que j'ai fait est assez 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
    }
}

Bonus: masquer la mise en oeuvre> en faire une collection

Cacher les détails de la mise en œuvre est toujours une bonne idée. Pour cela, vous aurez besoin d'un peu plus de code. L'astuce consiste à conformer DirectionsList à Collection et à rendre votre tableau interne list privé:

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

Vous pouvez en savoir plus sur la conformité aux collections personnalisées dans ce billet de blog de John Sundell: https://medium.com/@johnsundell/creating-custom-collections-in-Swift-a344e25d0bb

3
blackjacx