ฉันจะสร้าง Enum Decodable ได้อย่างไรใน swift 4


157
enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

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

ฉันจะทำอะไรให้สำเร็จ นอกจากนี้สมมติว่าฉันเปลี่ยนcase :

case image(value: Int)

ฉันจะทำให้สิ่งนี้สอดคล้องกับ Decodable ได้อย่างไร

แก้ไขนี่คือรหัสเต็มของฉัน (ซึ่งใช้งานไม่ได้)

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
}

การแก้ไขครั้งสุดท้าย นอกจากนี้มันจะจัดการกับ enum เช่นนี้ได้อย่างไร

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

คำตอบ:


262

มันค่อนข้างง่ายเพียงใช้StringหรือIntค่าดิบซึ่งกำหนดโดยปริยาย

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

imageถูกเข้ารหัส0และblobเพื่อ1

หรือ

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

imageถูกเข้ารหัส"image"และblobเพื่อ"blob"


นี่คือตัวอย่างง่ายๆวิธีใช้:

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
ฉันลองใช้รหัสที่คุณแนะนำ แต่ไม่ได้ผล ฉันแก้ไขโค้ดเพื่อแสดง JSON ฉันกำลังพยายามถอดรหัส
swift nub

8
Enum ไม่สามารถเข้ารหัส / ถอดรหัสได้เพียงอย่างเดียว มันจะต้องถูกฝังอยู่ใน struct ฉันเพิ่มตัวอย่าง
vadian

ฉันจะตั้งค่าสถานะนี้ให้ถูกต้อง แต่มีส่วนสุดท้ายในคำถามข้างต้นที่ไม่ได้รับคำตอบ เกิดอะไรขึ้นถ้า Enum ของฉันเป็นแบบนี้? (แก้ไขด้านบน)
swift nub

หากคุณใช้ enums กับประเภทที่เกี่ยวข้องคุณต้องเขียนวิธีการเข้ารหัสและถอดรหัสเอง โปรดอ่านการเข้ารหัสและการถอดรหัสประเภทที่กำหนดเอง
vadian

1
เกี่ยวกับ "Enum ที่ไม่สามารถ en- / ถอดรหัส แต่เพียงผู้เดียว." iOS 13.3ดูเหมือนว่าจะได้รับการแก้ไขที่ ฉันทดสอบiOS 13.3และiOS 12.4.3พวกมันก็ทำงานต่างกัน ภายใต้iOS 13.3enum สามารถ en- / ถอดรหัสเท่านั้น
AechoLiu

111

วิธีการทำให้ enums ที่มีประเภทที่เกี่ยวข้องสอดคล้องกับ Codable

คำตอบนี้คล้ายกับ @Howard Lovatt แต่หลีกเลี่ยงการสร้างPostTypeCodableFormstruct และใช้KeyedEncodingContainerชนิดที่มีให้โดย Appleเป็นคุณสมบัติในEncoderและDecoderแทนซึ่งจะช่วยลดการสร้าง

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

รหัสนี้ใช้ได้กับฉันใน 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)

ฉันรักคำตอบนี้! โปรดทราบว่าตัวอย่างนี้สะท้อนในโพสต์บน objc.io เกี่ยวกับการสร้างEitherรหัส
Ben Leggiero

คำตอบที่ดีที่สุด
Peter Suwara

38

Swift จะส่ง.dataCorruptedข้อผิดพลาดหากพบค่า enum ที่ไม่รู้จัก หากข้อมูลของคุณมาจากเซิร์ฟเวอร์ก็สามารถส่งค่า enum ที่ไม่รู้จักให้คุณได้ตลอดเวลา (ฝั่งเซิร์ฟเวอร์บั๊กชนิดใหม่ที่เพิ่มในเวอร์ชัน API และคุณต้องการให้แอพเวอร์ชันก่อนหน้าของคุณจัดการกับเคสอย่างสง่างาม ฯลฯ ) คุณควรเตรียมตัวให้ดีกว่าและเขียนโค้ด "รูปแบบการป้องกัน" เพื่อถอดรหัส enums ของคุณอย่างปลอดภัย

นี่คือตัวอย่างเกี่ยวกับวิธีการทำโดยมีหรือไม่มีค่าที่เกี่ยวข้อง

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

และวิธีใช้ในโครงสร้างปิดล้อม:

    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
ขอบคุณคำตอบของคุณเข้าใจง่ายกว่ามาก
DazChong

1
คำตอบนี้ได้ช่วยฉันด้วยขอบคุณ มันสามารถปรับปรุงได้โดยการทำให้การสืบทอด Enum ของคุณจาก String จากนั้นคุณไม่จำเป็นต้องสลับไปใช้สตริง
Gobe

27

หากต้องการขยายคำตอบของ @ Toka คุณอาจเพิ่มมูลค่าที่เป็นตัวแทนดิบให้กับ enum และใช้ตัวสร้างทางเลือกเริ่มต้นเพื่อสร้าง enum โดยไม่ต้อง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
  }
}

มันอาจจะขยายโดยใช้โปรโตคอลที่กำหนดเองที่ช่วยให้ refactor ตัวสร้าง:

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
}

นอกจากนี้ยังสามารถขยายได้อย่างง่ายดายสำหรับการโยนข้อผิดพลาดหากระบุค่า enum ที่ไม่ถูกต้องแทนที่จะใช้ค่าเริ่มต้น สรุปสาระสำคัญกับการเปลี่ยนแปลงนี้มีอยู่ที่นี่: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128
รหัสได้รับการรวบรวมและทดสอบโดยใช้ Swift 4.1 / Xcode 9.3


1
นี่คือคำตอบที่ฉันต้องการ
Nathan Hosselton

7

ตัวแปรของการตอบสนองของ @ proxpero ที่เป็น terser จะกำหนดตัวถอดรหัสเป็น:

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

สิ่งนี้อนุญาตให้คอมไพเลอร์ตรวจสอบเคสอย่างละเอียดและยังไม่ระงับข้อความแสดงข้อผิดพลาดสำหรับเคสที่ค่าที่เข้ารหัสไม่ตรงกับค่าที่คาดหวังของคีย์


ฉันเห็นด้วยว่าสิ่งนี้ดีกว่า
proxpero

6

ที่จริงแล้วคำตอบข้างต้นนั้นยอดเยี่ยมจริงๆ แต่พวกเขาขาดรายละเอียดบางอย่างสำหรับสิ่งที่หลาย ๆ คนต้องการในโครงการไคลเอนต์ / เซิร์ฟเวอร์ที่พัฒนาอย่างต่อเนื่อง เราพัฒนาแอพในขณะที่แบ็กเอนด์ของเราวิวัฒนาการอย่างต่อเนื่องเมื่อเวลาผ่านไปซึ่งหมายความว่าบางกรณี Enum จะเปลี่ยนวิวัฒนาการที่ ดังนั้นเราต้องการกลยุทธ์การถอดรหัส enum ที่สามารถถอดรหัสอาร์เรย์ของ enums ที่มีกรณีที่ไม่รู้จัก มิฉะนั้นการถอดรหัสวัตถุที่มีอาร์เรย์จะล้มเหลว

สิ่งที่ฉันทำง่ายมาก:

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

โบนัส: ซ่อนการนำไปใช้> ทำให้เป็นชุดสะสม

การซ่อนรายละเอียดการใช้งานเป็นความคิดที่ดีเสมอ สำหรับสิ่งนี้คุณต้องใช้รหัสเพิ่มอีกนิด เคล็ดลับคือเพื่อให้สอดคล้องDirectionsListไปCollectionและทำให้ภายในlistอาร์เรย์ส่วนตัว:

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

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับการปฏิบัติตามคอลเลกชันที่กำหนดเองในโพสต์บล็อกนี้โดย John Sundell: https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb0


5

คุณสามารถทำสิ่งที่คุณต้องการ แต่มันมีส่วนเกี่ยวข้อง :(

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)

แฮ็คที่น่าสนใจ
Roman Filippov
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.