简体   繁体   English

Swift 4 中默认情况下的可编码枚举

[英]Codable enum with default case in Swift 4

I have defined an enum as follows:我定义了一个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
}

that maps a JSON string property.映射一个 JSON 字符串属性。 The automatic serialization and deserialization works fine, but I found that if a different string is encountered, the deserialization fails.自动序列化和反序列化工作正常,但是我发现如果遇到不同的字符串,反序列化会失败。

Is it possible to define an unknown case that maps any other available case?是否可以定义一个映射任何其他可用案例的unknown案例?

This can be very useful, since this data comes from a RESTFul API that, maybe, can change in the future.这可能非常有用,因为这些数据来自 RESTFul API,未来可能会发生变化。

You can extend your Codable Type and assign a default value in case of failure:您可以扩展您的Codable类型并在失败时分配默认值:

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

edit/update:编辑/更新:

Xcode 11.2 • Swift 5.1 or later Xcode 11.2 • Swift 5.1 或更高版本

Create a protocol that defaults to last case of a CaseIterable & Decodable enumeration:创建一个默认为CaseIterable & Decodable枚举的最后一个案例的协议:

protocol CaseIterableDefaultsLast: Decodable & CaseIterable & RawRepresentable
where RawValue: Decodable, AllCases: BidirectionalCollection { }

extension CaseIterableDefaultsLast {
    init(from decoder: Decoder) throws {
        self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? Self.allCases.last!
    }
}

Playground testing:游乐场测试:

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

let types = try! JSONDecoder().decode([Type].self , from: Data(#"["text","image","sound"]"#.utf8))  // [text, image, unknown]

You can drop the raw type for your Type and make unknown case that handles associated value.您可以删除Type的原始类型,并制作处理关联值的未知大小写。 But this comes at a cost.但这是有代价的。 You somehow need the raw values for your cases.您以某种方式需要您的案例的原始值。 Inspired from this and this SO answers I came up with this elegant solution to your problem.受到这个这个SO 答案的启发,我想出了这个优雅的解决方案来解决你的问题。

To be able to store the raw values , we will maintain another enum, but as 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
    }
}

Move the encoding & decoding part to extensions:encodingdecoding部分移至扩展:

Decodable part:可解码部分:

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

Encodable part:可编码部分:

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

Examples:例子:

I just wrapped it in a container structure(because we'll be using JSONEncoder/JSONDecoder) as:我只是将它包装在一个容器结构中(因为我们将使用 JSONEncoder/JSONDecoder)作为:

struct Root: Codable {
    let type: Type
}

For values other than unknown case:对于未知情况以外的值:

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

For values with unknown case:对于大小写未知的值:

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

I put the example with local objects.我将示例与本地对象放在一起。 You can try with your REST API response.您可以尝试使用 REST API 响应。

enum Type: String, Codable, Equatable {
    case image
    case document
    case unknown

    public init(from decoder: Decoder) throws {
        guard let rawValue = try? decoder.singleValueContainer().decode(String.self) else {
            self = .unknown
            return
        }
        self = Type(rawValue: rawValue) ?? .unknown
    }
}

Here's an alternative based on nayem 's answer that offers a slightly more streamlined syntax by using optional binding of the inner RawValues initialization:这是基于nayem的答案的替代方案,它通过使用内部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)
        }
    }
}

If you are certain that all your existing enum case names match the underlying string values they represent, you could streamline RawValue to:如果您确定所有现有的枚举案例名称都与它们所代表的基础字符串值匹配,您可以将RawValue简化为:

private enum RawValue: String, Codable {

    case a, b, c
}

...and encode(to:) to: ...并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)
    }
}

Here's a practical example of using this, eg, you want to model SomeValue that has a property you want to model as an enum:这是一个使用它的实际示例,例如,您想要对具有要建模为枚举的属性的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"}]
 */

You have to implement the init(from decoder: Decoder) throws initializer and check for a valid value:您必须实现init(from decoder: Decoder) throws initializer 并检查有效值:

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
    }

}

Add this extension and set YourEnumName .添加此扩展并设置YourEnumName

extension <#YourEnumName#>: Codable {
    public init(from decoder: Decoder) throws {
        self = try <#YourEnumName#>(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
    }
}

@LeoDabus thanks for your answers. @LeoDabus 感谢您的回答。 I modified them a bit to make a protocol for String enums that seems to work for me:我对它们进行了一些修改,以制作一个似乎对我有用的字符串枚举协议:

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

Let's start with a test case.让我们从一个测试用例开始。 We expect this to pass:我们希望这会通过:

    func testCodableEnumWithUnknown() throws {
        enum Fruit: String, Decodable, CodableEnumWithUnknown {
            case banana
            case apple

            case unknown
        }
        struct Container: Decodable {
            let fruit: Fruit
        }
        let data = #"{"fruit": "orange"}"#.data(using: .utf8)!
        let val = try JSONDecoder().decode(Container.self, from: data)
        XCTAssert(val.fruit == .unknown)
    }

Our protocol CodableEnumWithUnknown denotes the support of the unknown case that should be used by the decoder if an unknown value arises in the data.我们的协议CodableEnumWithUnknown表示支持unknown情况,如果数据中出现未知值,解码器应该使用这种情况。

And then the solution:然后解决方案:

public protocol CodableEnumWithUnknown: Codable, RawRepresentable {
    static var unknown: Self { get }
}

public extension CodableEnumWithUnknown where Self: RawRepresentable, Self.RawValue == String {

    init(from decoder: Decoder) throws {
        self = (try? Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))) ?? Self.unknown
    }
}

The trick is make your enum implement with the CodableEnumWithUnknown protocol and add the unknown case.诀窍是使您的枚举使用CodableEnumWithUnknown协议实现并添加unknown情况。

I favor this solution above using the .allCases.last!我喜欢上面使用.allCases.last! implementation mentioned in other posts, because i find them a bit brittle, as they are not typechecked by the compiler.其他帖子中提到的实现,因为我发现它们有点脆弱,因为编译器没有对它们进行类型检查。

You can use this extension to encode / decode (this snippet supports Int an String RawValue type enums, but can be easy extended to fit other types)您可以使用此扩展进行编码/解码(此代码段支持 Int 和 String RawValue 类型的枚举,但可以轻松扩展以适应其他类型)

extension NSCoder {
    
    func encodeEnum<T: RawRepresentable>(_ value: T?, forKey key: String) {
        guard let rawValue = value?.rawValue else {
            return
        }
        if let s = rawValue as? String {
            encode(s, forKey: key)
        } else if let i = rawValue as? Int {
            encode(i, forKey: key)
        } else {
            assert(false, "Unsupported type")
        }
    }
    
    func decodeEnum<T: RawRepresentable>(forKey key: String, defaultValue: T) -> T {
        if let s = decodeObject(forKey: key) as? String, s is T.RawValue {
            return T(rawValue: s as! T.RawValue) ?? defaultValue
        } else {
            let i = decodeInteger(forKey: key)
            if i is T.RawValue {
                return T(rawValue: i as! T.RawValue) ?? defaultValue
            }
        }
        return defaultValue
    }
    
}

than use it比使用它

// encode
coder.encodeEnum(source, forKey: "source")
// decode
source = coder.decodeEnum(forKey: "source", defaultValue: Source.home)

the following method will decode all types of enums with RawValue of type Decodable (Int, String, ..) and returns nil if it fails.以下方法将使用 Decodable (Int, String, ..) 类型的 RawValue 解码所有类型的枚举,如果失败则返回 nil。 This will prevent crashes caused by non-existent raw values inside the JSON response.这将防止由于 JSON 响应中不存在的原始值导致的崩溃。

Definition:定义:

extension Decodable {
    static func decode<T: RawRepresentable, R, K: CodingKey>(rawValue _: R.Type, forKey key: K, decoder: Decoder) throws -> T? where T.RawValue == R, R: Decodable {
        let container = try decoder.container(keyedBy: K.self)
        guard let rawValue = try container.decodeIfPresent(R.self, forKey: key) else { return nil }
        return T(rawValue: rawValue)
    }
}

Usage:用法:

enum Status: Int, Decodable {
        case active = 1
        case disabled = 2
    }
    
    struct Model: Decodable {
        let id: String
        let status: Status?
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            id = try container.decodeIfPresent(String.self, forKey: .id)
            status = try .decode(rawValue: Int.self, forKey: .status, decoder: decoder)
        }
    }

// status: -1 reutrns nil
// status:  2 returns .disabled 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM