Wie mache ich eine Enumeration in Swift 4 decodierbar?

154
enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

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

Was setze ich, um dies zu vervollständigen? Nehmen wir auch an, ich habe das geändert case:

case image(value: Int)

Wie kann ich dies an Decodable anpassen?

EDit Hier ist mein vollständiger Code (der nicht funktioniert)

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 Wie wird es mit einer solchen Aufzählung umgehen?

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

Antworten:

259

Es ist ziemlich einfach, nur implizite Werte zu verwenden Stringoder zu verwenden Int.

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

imageist codiert zu 0und blobzu1

Oder

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

imageist codiert zu "image"und blobzu"blob"


Dies ist ein einfaches Beispiel für die Verwendung:

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
quelle
1
Ich habe den von Ihnen vorgeschlagenen Code ausprobiert, aber er funktioniert nicht. Ich habe meinen Code bearbeitet, um den JSON anzuzeigen, den ich zu dekodieren versuche
Swift Nub
7
Eine Aufzählung kann nicht allein en- / decodiert werden. Es muss in eine Struktur eingebettet sein. Ich habe ein Beispiel hinzugefügt.
Vadian
Ich werde dies als korrekt kennzeichnen. Hatte aber einen letzten Teil in der obigen Frage, der nicht beantwortet wurde. Was ist, wenn meine Aufzählung so aussieht? (oben bearbeitet)
Swift Nub
Wenn Sie Aufzählungen mit zugehörigen Typen verwenden, müssen Sie benutzerdefinierte Codierungs- und Decodierungsmethoden schreiben. Bitte lesen Sie Kodierung und Dekodierung von benutzerdefinierten Typen
vadian
Über "Eine Aufzählung kann nicht allein de- / decodiert werden.", Scheint es bei gelöst zu sein iOS 13.3. Ich teste in iOS 13.3und iOS 12.4.3sie verhalten sich anders. Unter iOS 13.3kann enum nur en- / decodiert werden.
AechoLiu
110

So erstellen Sie Enums mit zugeordneten Typen Codable

Diese Antwort ähnelt der von @Howard Lovatt, vermeidet jedoch das Erstellen einer PostTypeCodableFormStruktur und verwendet stattdessen den von Apple bereitgestelltenKeyedEncodingContainer Typ als Eigenschaft für und , wodurch die Boilerplate reduziert wird.EncoderDecoder

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

Dieser Code funktioniert für mich auf 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
quelle
Ich liebe diese Antwort! Als Hinweis ist dieses Beispiel auch in hallte post auf objc.io darum, Eithercodierbar
Ben Leggiero
Die beste Antwort
Peter Suwara
38

Swift würde einen .dataCorruptedFehler auslösen, wenn ein unbekannter Aufzählungswert auftritt. Wenn Ihre Daten von einem Server stammen, können Sie jederzeit einen unbekannten Aufzählungswert erhalten (Fehler auf der Serverseite, neuer Typ in einer API-Version hinzugefügt und Sie möchten, dass die vorherigen Versionen Ihrer App den Fall ordnungsgemäß behandeln usw.). Sie sollten besser vorbereitet sein und "defensiven Stil" codieren, um Ihre Aufzählungen sicher zu dekodieren.

Hier ist ein Beispiel, wie es geht, mit oder ohne zugehörigen Wert

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

Und wie man es in einer umschließenden Struktur verwendet:

    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
quelle
1
Vielen Dank, Ihre Antwort ist viel einfacher zu verstehen.
DazChong
1
Diese Antwort hat mir auch geholfen, danke. Es kann verbessert werden, indem Sie Ihre Aufzählung von String erben lassen, dann müssen Sie keine Strings umschalten
Gobe
27

Um die Antwort von @ Toka zu erweitern, können Sie der Aufzählung auch einen rohen darstellbaren Wert hinzufügen und den optionalen Standardkonstruktor verwenden, um die Aufzählung ohne 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
  }
}

Es kann mithilfe eines benutzerdefinierten Protokolls erweitert werden, mit dem der Konstruktor umgestaltet werden kann:

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
}

Es kann auch leicht erweitert werden, um einen Fehler auszulösen, wenn ein ungültiger Aufzählungswert angegeben wurde, anstatt einen Wert als Standard festzulegen. Das Wesentliche zu dieser Änderung finden Sie hier: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128 .
Der Code wurde mit Swift 4.1 / Xcode 9.3 kompiliert und getestet.

Stéphane Copin
quelle
1
Dies ist die Antwort, nach der ich gesucht habe.
Nathan Hosselton
7

Eine Variante der Antwort von @ proxpero, die kürzer ist, besteht darin, den Decoder wie folgt zu formulieren:

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

Dies ermöglicht dem Compiler eine umfassende Überprüfung der Fälle und unterdrückt auch nicht die Fehlermeldung für den Fall, dass der codierte Wert nicht mit dem erwarteten Wert des Schlüssels übereinstimmt.

marcprux
quelle
Ich stimme zu, dass dies besser ist.
Proxpero
6

Eigentlich sind die obigen Antworten wirklich großartig, aber es fehlen einige Details für das, was viele Leute in einem kontinuierlich entwickelten Client / Server-Projekt benötigen. Wir entwickeln eine App, während sich unser Backend im Laufe der Zeit kontinuierlich weiterentwickelt, was bedeutet, dass einige Enum-Fälle diese Entwicklung ändern werden. Wir brauchen also eine Enum-Decodierungsstrategie, die Arrays von Enums dekodieren kann, die unbekannte Fälle enthalten. Andernfalls schlägt das Dekodieren des Objekts, das das Array enthält, einfach fehl.

Was ich getan habe, ist ganz einfach:

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: Implementierung ausblenden> Machen Sie es zu einer Sammlung

Implementierungsdetails zu verbergen ist immer eine gute Idee. Dafür benötigen Sie nur ein bisschen mehr Code. Der Trick besteht darin, sich DirectionsListan CollectionIhr internes listArray anzupassen und es privat zu machen:

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

Weitere Informationen zum Anpassen an benutzerdefinierte Sammlungen finden Sie in diesem Blogbeitrag von John Sundell: https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb0

blackjacx
quelle
5

Sie können tun, was Sie wollen, aber es ist ein bisschen kompliziert :(

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
quelle
interessanter Hack
Roman Filippov