Bagaimana cara membuat Enum Decodable di swift 4?

157
enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

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

Apa yang saya masukkan untuk menyelesaikan ini? Juga, katakanlah saya mengubah caseini:

case image(value: Int)

Bagaimana cara membuat ini sesuai dengan Decodable?

Sunting Di Sini adalah kode lengkap saya (yang tidak berfungsi)

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
}

Edit Final Juga, bagaimana ia menangani enum seperti ini?

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

Jawaban:

262

Ini cukup mudah, cukup gunakan Stringatau Intnilai mentah yang ditetapkan secara implisit.

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

imagedikodekan ke 0dan blobke1

Atau

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

imagedikodekan ke "image"dan blobke"blob"


Ini adalah contoh sederhana cara menggunakannya:

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
sumber
1
saya mencoba kode yang Anda sarankan, tetapi tidak berhasil. Saya telah mengedit kode saya untuk menunjukkan JSON yang saya coba dekode
swift nub
8
Enum tidak dapat didekodekan hanya. Itu harus tertanam dalam sebuah struct. Saya menambahkan contoh.
vadian
Saya akan menandai ini sebagai benar. Tetapi ada satu bagian terakhir dalam pertanyaan di atas yang tidak dijawab. Bagaimana jika enum saya terlihat seperti ini? (diedit di atas)
Swift nub
Jika Anda menggunakan enum dengan tipe terkait Anda harus menulis metode penyandian dan pengodean kustom. Silakan baca Pengodean dan
Penguraian
1
Tentang "Sebuah enum tidak bisa en- / diterjemahkan semata-mata.", Tampaknya akan diselesaikan pada iOS 13.3. Saya menguji iOS 13.3dan iOS 12.4.3, mereka berperilaku berbeda. Di bawah iOS 13.3, enum dapat en- / diterjemahkan hanya.
AechoLiu
111

Cara membuat enum dengan tipe terkait sesuai dengan Codable

Jawaban ini mirip dengan @Howard Lovatt tetapi menghindari membuat PostTypeCodableFormstruct dan sebaliknya menggunakan KeyedEncodingContainerjenis yang disediakan oleh Apple sebagai properti Encoderdan Decoder, yang mengurangi boilerplate.

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

Kode ini berfungsi untuk saya di 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
sumber
Saya suka jawaban ini! Sebagai catatan, contoh ini juga digaungkan dalam sebuah posting tentang objc.io tentang pembuatan Eithercodable
Ben Leggiero
Jawaban terbaik
Peter Suwara
38

Swift akan melempar .dataCorruptedkesalahan jika menemui nilai enum yang tidak diketahui. Jika data Anda berasal dari server, ia dapat mengirimi Anda nilai enum yang tidak diketahui kapan saja (sisi server bug, jenis baru ditambahkan dalam versi API dan Anda ingin versi aplikasi Anda sebelumnya menangani kasus ini dengan anggun, dll), Anda sebaiknya siap, dan kode "gaya defensif" untuk mendekodekan enum Anda dengan aman.

Berikut adalah contoh cara melakukannya, dengan atau tanpa nilai terkait

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

Dan cara menggunakannya dalam struct tertutup:

    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
sumber
1
Terima kasih, jawaban Anda jauh lebih mudah dimengerti.
DazChong
1
Jawaban ini telah membantu saya juga, terima kasih. Itu dapat ditingkatkan dengan membuat enum Anda mewarisi dari String, maka Anda tidak perlu beralih string
Gobe
27

Untuk memperluas jawaban @ Toka, Anda juga dapat menambahkan nilai representatif mentah ke enum, dan menggunakan konstruktor opsional default untuk membangun enum tanpa 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
  }
}

Itu dapat diperluas menggunakan protokol khusus yang memungkinkan untuk refactor konstruktor:

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
}

Itu juga dapat dengan mudah diperpanjang untuk melempar kesalahan jika nilai enum yang tidak valid ditentukan, daripada default pada nilai. Inti dari perubahan ini tersedia di sini: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128 .
Kode ini dikompilasi dan diuji menggunakan Swift 4.1 / Xcode 9.3.

Stéphane Copin
sumber
1
Inilah jawaban yang saya cari.
Nathan Hosselton
7

Varian dari respon @ proxpero yang terser adalah merumuskan decoder sebagai:

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

Ini memungkinkan kompiler untuk memverifikasi kasus secara mendalam, dan juga tidak menekan pesan kesalahan untuk kasus di mana nilai yang dikodekan tidak cocok dengan nilai yang diharapkan kunci.

marcprux
sumber
Saya setuju bahwa ini lebih baik.
proxpero
6

Sebenarnya jawaban di atas benar-benar hebat, tetapi mereka kehilangan beberapa perincian untuk apa yang dibutuhkan banyak orang dalam proyek klien / server yang terus dikembangkan. Kami mengembangkan aplikasi sementara backend kami terus berkembang dari waktu ke waktu, yang berarti beberapa kasus enum akan mengubah evolusi itu. Jadi kita memerlukan strategi decoding enum yang dapat men-decode array enum yang berisi kasus yang tidak diketahui. Kalau tidak, decoding objek yang berisi array hanya gagal.

Apa yang saya lakukan cukup sederhana:

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: Sembunyikan implementasi> Jadikan Koleksi

Menyembunyikan detail implementasi selalu merupakan ide yang bagus. Untuk ini, Anda akan memerlukan sedikit lebih banyak kode. Caranya adalah dengan menyesuaikan DirectionsListuntuk Collectiondan membuat internal Anda listarray yang pribadi:

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

Anda dapat membaca lebih lanjut tentang menyesuaikan dengan koleksi khusus dalam posting blog ini oleh John Sundell: https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb00

blackjacx
sumber
5

Anda dapat melakukan apa yang Anda inginkan, tetapi ini sedikit melibatkan :(

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
sumber
hack yang menarik
Roman Filippov