用于字符串资源格式的自定义 Swift 编码器/解码器
Posted
技术标签:
【中文标题】用于字符串资源格式的自定义 Swift 编码器/解码器【英文标题】:Custom Swift Encoder/Decoder for the Strings Resource Format 【发布时间】:2017-12-23 11:13:23 【问题描述】:我一直在玩 Codable
并从文件中读取和写入 JSON。现在我想写一个自定义的Coder
,可以读写ios的.strings
文件。谁能帮我这个?我找到了协议Encoder
和Decoder
,但我不知道我应该在这里实现什么:
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 编码器/解码器的主要内容,如果未能解决你的问题,请参考以下文章