Як зробити перемикання з перерахунком у швидкому 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)

Як я можу зробити це відповідно до декодируемого?

Редагувати Ось мій повний код (який не працює)

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 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
Перерахунок не може бути розроблений / розшифрований виключно. Він повинен бути вбудований в структуру. Я додав приклад.
вадіан

Я позначу це правильним. Але в останньому питанні було останнє місце, на яке не було відповіді. Що робити, якщо мій перерахунок виглядав так? (відредаговано вище)
стрімкий нуб

Якщо ви використовуєте переписки з пов'язаними типами, вам потрібно написати спеціальні методи кодування та декодування. Будь ласка, прочитайте користувальницькі типи кодування та розшифровки
vadian

1
Про те, що "перерахунок не може бути розшифрований / розшифрований виключно", схоже, це вирішено на iOS 13.3. Я тестую, iOS 13.3і iOS 12.4.3вони поводяться інакше. Згідно з перерахунком iOS 13.3, enum може бути перероблений / розшифрований виключно.
AechoLiu

111

Як зробити перерахунки із суміжними типами відповідними Codable

Ця відповідь схожа з характеристикою @Howard Lovatt, але уникає створення PostTypeCodableFormструктури, а натомість використовує 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кодируемим
Бен Леджіеро,

Найкраща відповідь
Пітер Сувара

38

Swift видасть .dataCorruptedпомилку, якщо зустріне невідоме значення перерахунку. Якщо ваші дані надходять із сервера, вони можуть надіслати вам невідоме значення перерахунку в будь-який час (сторона сервера помилок, новий тип доданий у версії API, і ви хочете, щоб попередні версії вашої програми граціозно вирішували цю справу тощо), вам краще бути готовим і ввести код "оборонний стиль", щоб безпечно розшифрувати свої перерахунки.

Ось приклад того, як це зробити, із пов'язаним значенням або без нього

    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
Ця відповідь мені теж допомогла, дякую. Це можна вдосконалити, зробивши перерахунок спадку від String, тоді вам не потрібно перемикати рядки
Gobe

27

Щоб поширити відповідь на @ Toka, ви можете також додати перелічене значення, що відображається, і використовувати необов'язковий конструктор за замовчуванням для побудови перерахунку без 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
  }
}

Він може бути розширений за допомогою спеціального протоколу, який дозволяє переробляти конструктор:

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
}

Він також може бути легко розширений для викидання помилки, якщо було вказано недійсне значення перерахунку, а не за замовчуванням значення. Зміст із цією зміною доступний тут: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128 .
Код був складений та протестований за допомогою Swift 4.1 / Xcode 9.3.


1
Це відповідь, яку я прийшов шукати.
Натан Хоссельтон

7

Варіантом реакції @ проперора, який є кращим, було б сформулювати декодер як:

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

Це дозволяє компілятору вичерпно перевірити випадки, а також не пригнічує повідомлення про помилку для випадку, коли закодоване значення не відповідає очікуваному значенню ключа.


Я згоден, що це краще.
проперо

6

Насправді відповіді вище справді чудові, але в них відсутні деякі деталі для того, що потрібно багатьом людям у проекті, що постійно розвивається клієнт / сервер. Ми розробляємо додаток, в той час як наш бекенд постійно розвивається з часом, а це означає, що деякі випадки перетворення можуть змінити цю еволюцію. Тому нам потрібна стратегія декодування enum, яка здатна розшифрувати масиви переліків, які містять невідомі випадки. В іншому випадку декодування об'єкта, що містить масив, просто не вдається.

Що я зробив досить просто:

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

Детальніше про відповідність колекціям користувача можна прочитати у цьому дописі Джона Сундела: 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)

цікавий хак
Роман Філіппов
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.