Làm cách nào để tạo enum Decodable trong swift 4?


157
enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

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

Tôi phải làm gì để hoàn thành điều này? Ngoài ra, giả sử tôi đã thay đổi caseđiều này:

case image(value: Int)

Làm thế nào để tôi thực hiện điều này phù hợp với Decodable?

EDit Đây là mã đầy đủ của tôi (không hoạt động)

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
}

Chỉnh sửa cuối cùng Ngoài ra, làm thế nào nó sẽ xử lý một enum như thế này?

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

Câu trả lời:


262

Nó khá dễ dàng, chỉ cần sử dụng Stringhoặc Intcác giá trị thô được gán ngầm.

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

imageđược mã hóa thành 0blobđến1

Hoặc là

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

imageđược mã hóa thành "image"blobđến"blob"


Đây là một ví dụ đơn giản về cách sử dụng nó:

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
Tôi đã thử mã mà bạn đề xuất, nhưng nó không hoạt động. Tôi đã chỉnh sửa mã của mình để hiển thị JSON tôi đang cố giải mã
swift nub

8
Một enum không thể được en- / giải mã duy nhất. Nó phải được nhúng trong một cấu trúc. Tôi đã thêm một ví dụ.
vadian

Tôi sẽ gắn cờ này là chính xác. Nhưng có một phần cuối cùng trong câu hỏi trên chưa được trả lời. Điều gì nếu enum của tôi trông như thế này? (chỉnh sửa ở trên)
swift nub

Nếu bạn đang sử dụng enums với các loại liên quan, bạn phải viết các phương thức mã hóa và giải mã tùy chỉnh. Vui lòng đọc các loại tùy chỉnh mã hóa và giải mã
vadian

1
Về "Một enum không thể được giải mã duy nhất.", Nó dường như được giải quyết tại iOS 13.3. Tôi kiểm tra iOS 13.3iOS 12.4.3, họ cư xử khác nhau. Theo iOS 13.3, enum chỉ có thể được giải mã.
AechoLiu

111

Làm thế nào để làm cho enum với các loại liên quan phù hợp với Codable

Câu trả lời này tương tự như @Howard Lovatt's nhưng tránh tạo ra một PostTypeCodableFormcấu trúc và thay vào đó sử dụng KeyedEncodingContainerloại do Apple cung cấp làm tài sản trên EncoderDecoder, làm giảm nồi hơi.

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

Mã này hoạt động với tôi trên 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)

Tôi thích câu trả lời này! Một lưu ý, ví dụ này cũng được lặp lại trong một bài đăng trên objc.io về việc tạo ra Eithermã hóa
Ben Leggiero

Câu trả lời hay nhất
Peter Suwara 13/03/19

38

Swift sẽ đưa ra một .dataCorruptedlỗi nếu nó gặp giá trị enum không xác định. Nếu dữ liệu của bạn đến từ máy chủ, nó có thể gửi cho bạn một giá trị enum không xác định bất cứ lúc nào (phía máy chủ lỗi, loại mới được thêm vào trong phiên bản API và bạn muốn các phiên bản trước của ứng dụng xử lý trường hợp một cách duyên dáng, v.v.), tốt hơn hết là bạn nên chuẩn bị và viết mã "phong cách phòng thủ" để giải mã an toàn cho những kẻ thù địch của bạn.

Dưới đây là một ví dụ về cách thực hiện, có hoặc không có giá trị liên quan

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

Và làm thế nào để sử dụng nó trong một cấu trúc kèm theo:

    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
Cảm ơn, câu trả lời của bạn dễ hiểu hơn nhiều.
DazChong

1
Câu trả lời này đã giúp tôi quá, cảm ơn. Nó có thể được cải thiện bằng cách làm cho enum của bạn được kế thừa từ Chuỗi, sau đó bạn không cần phải chuyển qua chuỗi
Gobe

27

Để mở rộng câu trả lời của @ Toka, bạn cũng có thể thêm một giá trị đại diện thô cho enum và sử dụng hàm tạo tùy chọn mặc định để xây dựng enum mà không có 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
  }
}

Nó có thể được mở rộng bằng giao thức tùy chỉnh cho phép cấu trúc lại hàm tạo:

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
}

Nó cũng có thể dễ dàng được mở rộng để ném lỗi nếu giá trị enum không hợp lệ được chỉ định, thay vì mặc định trên một giá trị. Gist với thay đổi này có sẵn ở đây: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128 .
Mã được biên dịch và kiểm tra bằng Swift 4.1 / Xcode 9.3.


1
Đây là câu trả lời tôi tìm kiếm.
Nathan Hosselton

7

Một biến thể của phản hồi @ proxpero sẽ được thiết kế để giải mã công thức là:

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

Điều này cho phép trình biên dịch xác minh triệt để các trường hợp và cũng không loại bỏ thông báo lỗi đối với trường hợp giá trị được mã hóa không khớp với giá trị mong đợi của khóa.


Tôi đồng ý rằng điều này là tốt hơn.
proxpero

6

Trên thực tế, các câu trả lời ở trên thực sự rất hay, nhưng chúng thiếu một số chi tiết cho những gì nhiều người cần trong một dự án máy khách / máy chủ được phát triển liên tục. Chúng tôi phát triển một ứng dụng trong khi phần phụ trợ của chúng tôi liên tục phát triển theo thời gian, điều đó có nghĩa là một số trường hợp enum sẽ thay đổi sự tiến hóa đó. Vì vậy, chúng ta cần một chiến lược giải mã enum có thể giải mã các mảng của enum có chứa các trường hợp không xác định. Mặt khác, giải mã đối tượng chứa mảng đơn giản là thất bại.

Những gì tôi đã làm khá đơn giản:

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

Phần thưởng: Ẩn thực hiện> Biến nó thành Bộ sưu tập

Để ẩn chi tiết thực hiện luôn là một ý tưởng tốt. Đối với điều này, bạn sẽ cần thêm một chút mã. Bí quyết là để phù hợp DirectionsListvới Collectionvà làm cho nội bộ của bạn listmảng tin:

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

Bạn có thể đọc thêm về việc tuân thủ các bộ sưu tập tùy chỉnh trong bài đăng trên blog này của John Sundell: https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb0


5

Bạn có thể làm những gì bạn muốn, nhưng nó có một chút liên quan :(

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 thú vị
Roman Filippov
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.