用于字符串资源格式的自定义 Swift 编码器/解码器

Posted

技术标签:

【中文标题】用于字符串资源格式的自定义 Swift 编码器/解码器【英文标题】:Custom Swift Encoder/Decoder for the Strings Resource Format 【发布时间】:2017-12-23 11:13:23 【问题描述】:

我一直在玩 Codable 并从文件中读取和写入 JSON。现在我想写一个自定义的Coder,可以读写ios.strings文件。谁能帮我这个?我找到了协议EncoderDecoder,但我不知道我应该在这里实现什么:

class StringsEncoder 

extension StringsEncoder: Encoder 
    var codingPath: [CodingKey?] 
        return []
    

    var userInfo: [CodingUserInfoKey : Any] 
        return [:]
    

    func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey 

    

    func unkeyedContainer() -> UnkeyedEncodingContainer 

    

    func singleValueContainer() -> SingleValueEncodingContainer 

    


extension StringsEncoder: Decoder 
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey 

    

    func unkeyedContainer() throws -> UnkeyedDecodingContainer 

    

    func singleValueContainer() throws -> SingleValueDecodingContainer 

    

【问题讨论】:

您是否查看过Codable 协议以及它们可以做什么?我不确定.strings 格式是否适合这个——Codable 协议必须支持具有数组、字典、数字、空值等的通用格式。.strings 文件不支持任何一个......这是一种用途非常单一的格式。 您可能可以,尽管对于字符串格式来说似乎有点矫枉过正。例如,查看JSONEncoder source,这是有据可查的。 swift unboxed 很快就会有一个类似于你正在寻找的帖子,我想 我知道你想了解 Codable,但如果你只是想读写字符串文件 checkout String.propertyListFromStringsFileFormat()Dictionary.descriptionInStringsFileFormat mikeash 也有一篇关于构建自定义可编码对象的非常好的帖子:mikeash.com/pyblog/… JSONEncoder 的实现已移至here 【参考方案1】:

这里的聚会有点晚了,但考虑到这个问题的高票数,我觉得这可能对其他人有帮助/提供信息。 (但我不会真正了解这些代码在实践中的实际用途——请查看上面的 cmets。)

不幸的是,考虑到编码堆栈的灵活性和类型安全性,实现了一个新的编码解码解决方案,作为替代外部表示,远非一项微不足道的任务......所以让我们开始吧:

编码

让我们从实现所需strings file 外部表示的编码 部分开始。 (必要的类型将以自上而下的方式引入。)

与标准的JSONEncoder 类一样,我们需要引入一个类来公开/驱动我们的新编码 API。让我们称之为StringsEncoder

/// An object that encodes instances of a data type 
/// as strings following the simple strings file format.
public class StringsEncoder 
    
    /// Returns a strings file-encoded representation of the specified value. 
    public func encode<T: Encodable>(_ value: T) throws -> String 
        let stringsEncoding = StringsEncoding()
        try value.encode(to: stringsEncoding)
        return dotStringsFormat(from: stringsEncoding.data.strings)
    
    
    private func dotStringsFormat(from strings: [String: String]) -> String 
        var dotStrings = strings.map  "\"\($0)\" = \"\($1)\";" 
        dotStrings.sort()
        dotStrings.insert("/* Generated by StringsEncoder */", at: 0)
        return dotStrings.joined(separator: "\n")
    

接下来,我们需要提供一个符合核心Encoder协议的类型(例如struct):

fileprivate struct StringsEncoding: Encoder 
    
    /// Stores the actual strings file data during encoding.
    fileprivate final class Data 
        private(set) var strings: [String: String] = [:]
        
        func encode(key codingKey: [CodingKey], value: String) 
            let key = codingKey.map  $0.stringValue .joined(separator: ".")
            strings[key] = value
        
    
    
    fileprivate var data: Data
    
    init(to encodedData: Data = Data()) 
        self.data = encodedData
    

    var codingPath: [CodingKey] = []
    
    let userInfo: [CodingUserInfoKey : Any] = [:]
    
    func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> 
        var container = StringsKeyedEncoding<Key>(to: data)
        container.codingPath = codingPath
        return KeyedEncodingContainer(container)
    
    
    func unkeyedContainer() -> UnkeyedEncodingContainer 
        var container = StringsUnkeyedEncoding(to: data)
        container.codingPath = codingPath
        return container
   
    
    func singleValueContainer() -> SingleValueEncodingContainer 
        var container = StringsSingleValueEncoding(to: data)
        container.codingPath = codingPath
        return container
    


最后,我们需要处理所有 3 个编码容器类型:

KeyedEncodingContainer UnkeyedEncodingContainer SingleValueEncodingContainer
fileprivate struct StringsKeyedEncoding<Key: CodingKey>: KeyedEncodingContainerProtocol 

    private let data: StringsEncoding.Data
    
    init(to data: StringsEncoding.Data) 
        self.data = data
    
    
    var codingPath: [CodingKey] = []
    
    mutating func encodeNil(forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: "nil")
    
    
    mutating func encode(_ value: Bool, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: String, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value)
    
    
    mutating func encode(_ value: Double, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: Float, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: Int, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: Int8, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: Int16, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: Int32, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: Int64, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: UInt, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: UInt8, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: UInt16, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: UInt32, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode(_ value: UInt64, forKey key: Key) throws 
        data.encode(key: codingPath + [key], value: value.description)
    
    
    mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws 
        var stringsEncoding = StringsEncoding(to: data)
        stringsEncoding.codingPath.append(key)
        try value.encode(to: stringsEncoding)
    
    
    mutating func nestedContainer<NestedKey: CodingKey>(
        keyedBy keyType: NestedKey.Type,
        forKey key: Key) -> KeyedEncodingContainer<NestedKey> 
        var container = StringsKeyedEncoding<NestedKey>(to: data)
        container.codingPath = codingPath + [key]
        return KeyedEncodingContainer(container)
    
    
    mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer 
        var container = StringsUnkeyedEncoding(to: data)
        container.codingPath = codingPath + [key]
        return container
    
    
    mutating func superEncoder() -> Encoder 
        let superKey = Key(stringValue: "super")!
        return superEncoder(forKey: superKey)
    
    
    mutating func superEncoder(forKey key: Key) -> Encoder 
        var stringsEncoding = StringsEncoding(to: data)
        stringsEncoding.codingPath = codingPath + [key]
        return stringsEncoding
    

fileprivate struct StringsUnkeyedEncoding: UnkeyedEncodingContainer 

    private let data: StringsEncoding.Data
    
    init(to data: StringsEncoding.Data) 
        self.data = data
    
    
    var codingPath: [CodingKey] = []

    private(set) var count: Int = 0
    
    private mutating func nextIndexedKey() -> CodingKey 
        let nextCodingKey = IndexedCodingKey(intValue: count)!
        count += 1
        return nextCodingKey
    
    
    private struct IndexedCodingKey: CodingKey 
        let intValue: Int?
        let stringValue: String

        init?(intValue: Int) 
            self.intValue = intValue
            self.stringValue = intValue.description
        

        init?(stringValue: String) 
            return nil
        
    

    mutating func encodeNil() throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: "nil")
    
    
    mutating func encode(_ value: Bool) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: String) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value)
    
    
    mutating func encode(_ value: Double) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: Float) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: Int) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: Int8) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: Int16) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: Int32) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: Int64) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: UInt) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: UInt8) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: UInt16) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: UInt32) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode(_ value: UInt64) throws 
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    
    
    mutating func encode<T: Encodable>(_ value: T) throws 
        var stringsEncoding = StringsEncoding(to: data)
        stringsEncoding.codingPath = codingPath + [nextIndexedKey()]
        try value.encode(to: stringsEncoding)
    
    
    mutating func nestedContainer<NestedKey: CodingKey>(
        keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> 
        var container = StringsKeyedEncoding<NestedKey>(to: data)
        container.codingPath = codingPath + [nextIndexedKey()]
        return KeyedEncodingContainer(container)
    
    
    mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer 
        var container = StringsUnkeyedEncoding(to: data)
        container.codingPath = codingPath + [nextIndexedKey()]
        return container
    
    
    mutating func superEncoder() -> Encoder 
        var stringsEncoding = StringsEncoding(to: data)
        stringsEncoding.codingPath.append(nextIndexedKey())
        return stringsEncoding
    

fileprivate struct StringsSingleValueEncoding: SingleValueEncodingContainer 
    
    private let data: StringsEncoding.Data
    
    init(to data: StringsEncoding.Data) 
        self.data = data
    

    var codingPath: [CodingKey] = []
    
    mutating func encodeNil() throws 
        data.encode(key: codingPath, value: "nil")
    
    
    mutating func encode(_ value: Bool) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: String) throws 
        data.encode(key: codingPath, value: value)
    
    
    mutating func encode(_ value: Double) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: Float) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: Int) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: Int8) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: Int16) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: Int32) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: Int64) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: UInt) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: UInt8) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: UInt16) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: UInt32) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode(_ value: UInt64) throws 
        data.encode(key: codingPath, value: value.description)
    
    
    mutating func encode<T: Encodable>(_ value: T) throws 
        var stringsEncoding = StringsEncoding(to: data)
        stringsEncoding.codingPath = codingPath
        try value.encode(to: stringsEncoding)
    

显然,我对如何使用(非常!)简单的 strings 文件 格式对嵌套类型进行编码做出了一些设计决定。希望我的代码足够清晰,如果需要,应该很容易调整编码细节。

测试

一个简单的Codable 类型的简单测试:

struct Product: Codable 
    var name: String
    var price: Float
    var info: String


let iPhone = Product(name: "iPhone X", price: 1_000, info: "Our best iPhone yet!")

let stringsEncoder = StringsEncoder()
do 
    let stringsFile = try stringsEncoder.encode(iPhone)
    print(stringsFile)
 catch 
    print("Encoding failed: \(error)")

输出:

/* Generated by StringsEncoder */
"info" = "Our best iPhone yet!";
"name" = "iPhone X";
"price" = "1000.0";

使用嵌套结构数组进行更复杂的测试:

struct Product: Codable 
    var name: String
    var price: Float
    var info: String


struct Address: Codable 
    var street: String
    var city: String
    var state: String


struct Store: Codable 
    var name: String
    var address: Address // nested struct
    var products: [Product] // array


let iPhone = Product(name: "iPhone X", price: 1_000, info: "Our best iPhone yet!")
let macBook = Product(name: "Mac Book Pro", price: 2_000, info: "Early 2019")
let watch = Product(name: "Apple Watch", price: 500, info: "Series 4")

let appleStore = Store(
    name: "Apple Store",
    address: Address(street: "300 Post Street", city: "San Francisco", state: "CA"),
    products: [iPhone, macBook, watch]
)

let stringsEncoder = StringsEncoder()
do 
    let stringsFile = try stringsEncoder.encode(appleStore)
    print(stringsFile)
 catch 
    print("Encoding failed: \(error)")

输出:

/* Generated by StringsEncoder */
"address.city" = "San Francisco";
"address.state" = "CA";
"address.street" = "300 Post Street";
"name" = "Apple Store";
"products.0.info" = "Our best iPhone yet!";
"products.0.name" = "iPhone X";
"products.0.price" = "1000.0";
"products.1.info" = "Early 2019";
"products.1.name" = "Mac Book Pro";
"products.1.price" = "2000.0";
"products.2.info" = "Series 4";
"products.2.name" = "Apple Watch";
"products.2.price" = "500.0";

解码

鉴于这个答案已经有多大了,我将把 解码 部分(即创建StringsDecoder 类,符合Decoder 协议等)作为练习给读者...如果你们需要任何帮助,请告诉我,我稍后会发布完整的解决方案;)

【讨论】:

这太棒了。关于如何添加自定义日期格式编码的任何建议? 另外需要注意的是,String(describing:)应该被使用而不是.description @Patrick 谢谢 ;) 您可以在编码Encodable 对象的方法中添加自定义Date 格式(即检查value is Date)。然后,您可以将Date 转换为字符串——这是您的自定义日期格式!然后最后调用encode(to:)。您需要在三种容器类型中添加此逻辑以涵盖所有 Date 用法。最后,请务必检查JSONEncoder.DateEncodingStrategy 以了解将日期格式公开为编码器的可扩展 API 的好方法。 那么你和 Donald Knuth 有远亲关系吗? ? 对于个人项目,我正在为邮件标题编写解析器。作为学习更多的练习,我想使用 Decodable。我知道我可以通过其他方式轻松做到这一点,但正如我所说,我想通过 Decodable 做到这一点。我想我理解在谈论使用 JSONDecode 的自定义类型的文章中描述的常见内容,但我不理解 JSONDeocde 所做的部分?

以上是关于用于字符串资源格式的自定义 Swift 编码器/解码器的主要内容,如果未能解决你的问题,请参考以下文章

在 Swift 中,如何检测编码的自定义类数组中的重复项?

`convertFromSnakeCase` 策略不适用于 Swift 中的自定义 `CodingKeys`

在 Swift 中编码对象

swift中的自定义集合视图单元格

swift 字符串中的自定义样式字符

显示相机和画廊权限的自定义警报 swift 3