代码之家  ›  专栏  ›  技术社区  ›  LucaRoverelli

Swift 4中带默认大小写的可编码枚举

  •  61
  • LucaRoverelli  · 技术社区  · 6 年前

    我定义了 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,将来可能会更改。

    9 回复  |  直到 6 年前
        1
  •  158
  •   Leo Dabus    4 年前

    您可以扩展 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
        }
    }
    

    编辑/更新:

    Xcode 11.2 Swift 5.1或更高版本

    创建默认为 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!
        }
    }
    

    操场测试:

    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]
    
        2
  •  13
  •   nayem    6 年前

    您可以删除原始类型 Type 和make 未知的 处理关联值的大小写。但这是有代价的。您不知何故需要案例的原始值。灵感来自 this this 所以我为你的问题想出了一个优雅的解决方案。

    能够存储 原始值 ,我们将维护另一个枚举,但作为私有:

    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 & decoding 延伸部分:

    可解码部分:

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

    示例:

    我只是将其包装在一个容器结构中(因为我们将使用JSONECODER/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响应。

        3
  •  7
  •   hites    5 年前
    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
        }
    }
    
        4
  •  5
  •   Scott Gardner    6 年前

    这里有一个基于 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)
            }
        }
    }
    

    如果确定所有现有的枚举大小写名称都与它们所表示的底层字符串值匹配,则可以简化 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"}]
     */
    
        5
  •  2
  •   André Slotta    6 年前

    您必须实现 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
        }
    
    }
    
        6
  •  1
  •   Mohammad Mohammadi Nasrabadi    5 年前

    添加此扩展名并设置 YourEnumName

    extension <#YourEnumName#>: Codable {
        public init(from decoder: Decoder) throws {
            self = try <#YourEnumName#>(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
        }
    }
    
        7
  •  1
  •   LenK    5 年前

    @LeoDabus感谢您的回答。我对它们进行了一些修改,为字符串枚举制定了一个似乎适合我的协议:

    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
                }
            }
        }
    }
    
        8
  •  1
  •   jackx    3 年前

    让我们从一个测试用例开始。我们预计这将通过:

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

    我们的协议 CodableEnumWithUnknown 表示对 unknown 如果数据中出现未知值,解码器应使用的情况。

    然后是解决方案:

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

    诀窍是使用 CodableEnumWithUnknown 协议并添加 未知的 案例

    我赞成使用上面的解决方案 .allCases.last! 其他帖子中提到的实现,因为我发现它们有点脆弱,因为编译器没有对它们进行类型检查。

        9
  •  0
  •   Peter Lapisu    3 年前

    您可以使用此扩展来编码/解码 (此代码段支持Int-an字符串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
        }
        
    }
    

    而不是使用它

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