Вопрос:

Кодируемое перечисление с регистром по умолчанию в Swift 4

ios swift enums swift4 codable

3751 просмотра

6 ответа

83 Репутация автора

Я определил enumследующим образом:

enum Type: String, Codable {
    case text = "text"
    case image = "image"
    case document = "document"
    case profile = "profile"
    case sign = "sign"
    case inputDate = "input_date"
    case inputText = "input_text"
    case inputNumber = "input_number"
    case inputOption = "input_option"

    case unknown
}

который отображает строковое свойство JSON. Автоматическая сериализация и десериализация работает нормально, но я обнаружил, что при обнаружении другой строки десериализация завершается неудачно.

Можно ли определить unknownслучай, который отображает любой другой доступный случай?

Это может быть очень полезно, так как эти данные поступают из RESTFul API, который, возможно, может измениться в будущем.

Автор: LucaRoverelli Источник Размещён: 06.04.2018 03:06

Ответы (6)


2 плюса

10752 Репутация автора

Вы должны реализовать init(from decoder: Decoder) throwsинициализатор и проверить правильность значения:

struct SomeStruct: Codable {

    enum SomeType: String, Codable {
        case text
        case image
        case document
        case profile
        case sign
        case inputDate = "input_date"
        case inputText = "input_text"
        case inputNumber = "input_number"
        case inputOption = "input_option"

        case unknown
    }

    var someType: SomeType

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        someType = (try? values.decode(SomeType.self, forKey: .someType)) ?? .unknown
    }

}
Автор: André Slotta Размещён: 06.04.2018 04:20

39 плюса

142601 Репутация автора

Решение

Вы можете расширить свой Codable Type и назначить значение по умолчанию в случае сбоя:

enum Type: String {
    case text, image, document, profile, sign, inputDate = "input_date", inputText = "input_text" , inputNumber = "input_number", inputOption = "input_option", unknown
}
extension Type: Codable {
    public init(from decoder: Decoder) throws {
        self = try Type(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
    }
}
Автор: Leo Dabus Размещён: 06.04.2018 04:29

6 плюса

3750 Репутация автора

Вы можете отбросить необработанный тип для вашего Typeи создать неизвестный регистр, который обрабатывает связанное значение. Но это обходится дорого. Вам как-то нужны необработанные значения для ваших дел. Вдохновленный этим и этими SO ответами, я придумал элегантное решение вашей проблемы.

Чтобы иметь возможность хранить необработанные значения , мы будем поддерживать другое перечисление, но как private:

enum Type {
    case text
    case image
    case document
    case profile
    case sign
    case inputDate
    case inputText
    case inputNumber
    case inputOption
    case unknown(String)

    // Make this private
    private enum RawValues: String, Codable {
        case text = "text"
        case image = "image"
        case document = "document"
        case profile = "profile"
        case sign = "sign"
        case inputDate = "input_date"
        case inputText = "input_text"
        case inputNumber = "input_number"
        case inputOption = "input_option"
        // No such case here for the unknowns
    }
}

Переместить encoding& decodingpart в расширения:

Декодируемая часть:

extension Type: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        // As you already know your RawValues is String actually, you decode String here
        let stringForRawValues = try container.decode(String.self) 
        // This is the trick here...
        switch stringForRawValues { 
        // Now You can switch over this String with cases from RawValues since it is String
        case RawValues.text.rawValue:
            self = .text
        case RawValues.image.rawValue:
            self = .image
        case RawValues.document.rawValue:
            self = .document
        case RawValues.profile.rawValue:
            self = .profile
        case RawValues.sign.rawValue:
            self = .sign
        case RawValues.inputDate.rawValue:
            self = .inputDate
        case RawValues.inputText.rawValue:
            self = .inputText
        case RawValues.inputNumber.rawValue:
            self = .inputNumber
        case RawValues.inputOption.rawValue:
            self = .inputOption

        // Now handle all unknown types. You just pass the String to Type's unknown case. 
        // And this is true for every other unknowns that aren't defined in your RawValues
        default: 
            self = .unknown(stringForRawValues)
        }
    }
}

Кодируемая часть:

extension Type: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .text:
            try container.encode(RawValues.text)
        case .image:
            try container.encode(RawValues.image)
        case .document:
            try container.encode(RawValues.document)
        case .profile:
            try container.encode(RawValues.profile)
        case .sign:
            try container.encode(RawValues.sign)
        case .inputDate:
            try container.encode(RawValues.inputDate)
        case .inputText:
            try container.encode(RawValues.inputText)
        case .inputNumber:
            try container.encode(RawValues.inputNumber)
        case .inputOption:
            try container.encode(RawValues.inputOption)

        case .unknown(let string): 
            // You get the actual String here from the associated value and just encode it
            try container.encode(string)
        }
    }
}

Примеры:

Я просто обернул его в структуру контейнера (потому что мы будем использовать JSONEncoder / JSONDecoder) как:

struct Root: Codable {
    let type: Type
}

Для значений, отличных от неизвестного случая:

let rootObject = Root(type: Type.document)
do {
    let encodedRoot = try JSONEncoder().encode(rootObject)
    do {
        let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
        print(decodedRoot.type) // document
    } catch {
        print(error)
    }
} catch {
    print(error)
}

Для значений с неизвестным регистром:

let rootObject = Root(type: Type.unknown("new type"))
do {
    let encodedRoot = try JSONEncoder().encode(rootObject)
    do {
        let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
        print(decodedRoot.type) // unknown("new type")
    } catch {
        print(error)
    }
} catch {
    print(error)
}

Я поставил пример с местными объектами. Вы можете попробовать с вашим ответом REST API.

Автор: nayem Размещён: 06.04.2018 07:28

3 плюса

6825 Репутация автора

Вот альтернатива, основанная на ответе Найема , которая предлагает немного более упорядоченный синтаксис с использованием необязательного связывания внутренней RawValuesинициализации:

enum MyEnum: Codable {

    case a, b, c
    case other(name: String)

    private enum RawValue: String, Codable {

        case a = "a"
        case b = "b"
        case c = "c"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let decodedString = try container.decode(String.self)

        if let value = RawValue(rawValue: decodedString) {
            switch value {
            case .a:
                self = .a
            case .b:
                self = .b
            case .c:
                self = .c
            }
        } else {
            self = .other(name: decodedString)
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        switch self {
        case .a:
            try container.encode(RawValue.a)
        case .b:
            try container.encode(RawValue.b)
        case .c:
            try container.encode(RawValue.c)
        case .other(let name):
            try container.encode(name)
        }
    }
}

Если вы уверены, что все ваши существующие имена перечислений соответствуют базовым строковым значениям, которые они представляют, вы можете упростить RawValue:

private enum RawValue: String, Codable {

    case a, b, c
}

... и encode(to:)для:

func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()

    if let rawValue = RawValue(rawValue: String(describing: self)) {
        try container.encode(rawValue)
    } else if case .other(let name) = self {
        try container.encode(name)
    }
}

Вот практический пример использования этого, например, вы хотите смоделировать SomeValue, у которого есть свойство, которое вы хотите смоделировать как перечисление:

struct SomeValue: Codable {

    enum MyEnum: Codable {

        case a, b, c
        case other(name: String)

        private enum RawValue: String, Codable {

            case a = "a"
            case b = "b"
            case c = "letter_c"
        }

        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            let decodedString = try container.decode(String.self)

            if let value = RawValue(rawValue: decodedString) {
                switch value {
                case .a:
                    self = .a
                case .b:
                    self = .b
                case .c:
                    self = .c
                }
            } else {
                self = .other(name: decodedString)
            }
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()

            switch self {
            case .a:
                try container.encode(RawValue.a)
            case .b:
                try container.encode(RawValue.b)
            case .c:
                try container.encode(RawValue.c)
            case .other(let name):
                try container.encode(name)
            }
        }
    }

}

let jsonData = """
[
    { "value": "a" },
    { "value": "letter_c" },
    { "value": "c" },
    { "value": "Other value" }
]
""".data(using: .utf8)!

let decoder = JSONDecoder()

if let values = try? decoder.decode([SomeValue].self, from: jsonData) {
    values.forEach { print($0.value) }

    let encoder = JSONEncoder()

    if let encodedJson = try? encoder.encode(values) {
        print(String(data: encodedJson, encoding: .utf8)!)
    }
}


/* Prints:
 a
 c
 other(name: "c")
 other(name: "Other value")
 [{"value":"a"},{"value":"letter_c"},{"value":"c"},{"value":"Other value"}]
 */
Автор: Scott Gardner Размещён: 30.11.2018 08:27

2 плюса

21 Репутация автора

Добавьте это расширение и установите YourEnumName.

extension <#YourEnumName#>: Codable {
    public init(from decoder: Decoder) throws {
        self = try <#YourEnumName#>(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
    }
}
Автор: mohammad mNasrabadi Размещён: 29.04.2019 07:45

0 плюса

1443 Репутация автора

@LeoDabus спасибо за ваши ответы. Я немного изменил их, чтобы сделать протокол для перечислений String, который, кажется, работает для меня:

protocol CodableWithUnknown: Codable {}
extension CodableWithUnknown where Self: RawRepresentable, Self.RawValue == String {
    init(from decoder: Decoder) throws {
        do {
            try self = Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))!
        } catch {
            if let unknown = Self(rawValue: "unknown") {
                self = unknown
            } else {
                throw error
            }
        }
    }
}
Автор: LenK Размещён: 12.06.2019 06:41
Вопросы из категории :
32x32