可编码—改进解码JSON的4种方法
Swift 4中引入的Codable提供了一种方便,轻松的方式来编码和解码JSON。 但是,并不是所有的东西都开箱即用。
Swift 4中引入的Codable提供了一种方便,轻松的方式来编码和解码JSON。 但是,并不是所有的东西都开箱即用。 例如,让我们看一下Twitter中的JSON文件。 作为JSON解码器工作方式的结果,我们看到:
- 引用和标准推文使用相似但不完全相同的字段。
- 转换为属性名称的键不遵循Swift约定。
- 日期不是采用标准的接受格式。
- 颜色的格式不受任何Color类 (UIColor,NSColor等)接受。
因此,我们将学习如何为某些特殊情况设置您的Codable类型。 具体来说,我们将研究:
- 对可编码类型使用协议
- 不同的属性名称和键
- 属性值中的日期
- 自定义类型的属性值
要继续学习,您可以在GitHub上查看本文的Xcode游乐场。
JSONDecoder的工作方式
这些是JSONDecoder如何将JSON转换为结构化类型的基本规则:
- 属性名称按原样转换。
- 默认情况下,简单属性值将转换为
String
,Int
或Double
- 遵循正确格式的属性值可以解码为
URL
,Data
或Date
。 - 任何属性值都可以认为是
Optional
- 方括号
[]
值将转换为Array
- 花括号
{}
值将转换为Dictionary
或自定义类型。 - 所有需要解码的类型都需要实现Decodable协议。 同样,需要编码的类型需要实现可编码协议。 如果需要同时对类型进行解码和编码,则可以简单地实现Codable。
在这种情况下,我们将查看为一条推文返回的Twitter JSON数据。 因此, 我们要做的第一件事就是为每种返回的数据类型创建一些基本结构 。 结果,您可以在仓库的此分支上查看解码工作的第一步。
即使数据相似,我们也会创建单独的结构以提供更大的灵活性。 但是,这并不意味着我们不能使用协议来简化功能中这些类型的使用。
将协议与可编码一起使用
例如,在Twitter API中,始终使用tweet的概念。 举例来说,他们的JSON有一条主推文和一条引号 。 因此,可能倾向于对两者使用相同的结构。 但是,更好的方法是两个单独的结构:
public struct Tweet:可编码{
public let created_at:日期
公开让ID:Int
public let full_text:字符串
public let display_text_range:[Int]
公共租赁实体:TweetEntities
public let source:字符串
public let in_reply_to_status_id:整数?
public let in_reply_to_user_id:整数?
public let in_reply_to_screen_name:字符串
公开让用户:TweetUser
公开让quoted_status:QuotedTweet吗?
公开让is_quote_status:布尔
public let retweet_count:整数
public let favorite_count:整数
公众喜欢:布尔
公开让转推了:布尔
公开让可能_敏感:布尔
公开let lets_sensitive_appealable:布尔
public let lang:字符串
}
公共结构QuotedTweet:可编码{
public let created_at:日期
公开让ID:
public let full_text:字符串
public let display_text_range:[Int]
公共租赁实体:TweetEntities
公开让用户:TweetUser
public let source:字符串
public letextended_entities:TweetEntities
公开让is_quote_status:布尔
public let retweet_count:整数
public let favorite_count:整数
公众喜欢:布尔
公开让转推了:布尔
公开让可能_敏感:布尔
公开let lets_sensitive_appealable:布尔
public let lang:字符串
}
因此,这意味着重复的字段。 但是,它也允许更好的灵活性和更容易的JSON解码 。 另一方面,当在函数中用作参数时,我们可以简化这些类型。 例如,假设我们需要打印推文:
func printTweet(_ tweet:Tweet){
打印(tweet.full_text)
如果让quoted_status = tweet.quoted_status {
printTweet(quoted_status)
}
}
func printTweet(_ tweet:QuotedTweet){
打印(“>”,tweet.full_text)
}
但是,使用一些基本的面向协议的编程 ,我们实际上可以对此进行优化。 首先,我们对一个函数进行存根,该函数根据协议打印出该推文,而不管它是否是引用的推文 。
func printTweet(_ tweet:TweetProtocol,withQuoteLevel级别:Int = 0){
print(String(repeating:“>”,count:level),tweet.full_text)
如果让quotedTweet = tweet.quotedTweet {
printTweet(quotedTweet,withQuoteLevel:level + 1)
}
}
因此,我们创建了具有上面功能所需属性的协议。
公共协议TweetProtocol {
var full_text:字符串{get}
var quotedTweet:TweetProtocol? {得到}
}
最后,我们为将用于Tweet
和QuotedTweet
的两种类型实现协议:
扩展名Tweet:TweetProtocol {
public var quotedTweet:Tweet协议? {
返回self.quoted_status
}
}
扩展QuotedTweet:TweetProtocol {
public var quotedTweet:Tweet协议? {
返回零
}
}
现在, 我们可以更好地与Codable一起使用,同时使用Protocols添加类似的功能。 接下来,让我们清理属性名称。
自定义属性名称
关于Twitter的JSON,您注意到的一件事是, 它们使用所谓的 蛇形大写字母 来组合属性名称中的单词。 结果,下划线 _
用于组合JSON密钥中的单词。 例如,一条推文全文的属性称为full_text
。 但是, Swift API设计指南 建议使用 驼峰式大小写。 骆驼案将每个新词大写。 因此,在这种情况下,我们的属性名称将为fullText
。 因此,我们有两种解决方法: 自定义CodingKeys或使用KeyEncodingStrategy。
自定义CodingKeys通过允许我们提供从JSON键到属性名的映射,为我们提供了最大的灵活性。 但是,在这种情况下, 存在将所有键映射到属性名称的一致策略。 因此,在这种情况下,我们将使用KeyDecodingStrategy
特别是KeyDecodingStrategy.convertFromSnakeCase
:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
另外,如果KeyDecodingStrategy.convertFromSnakeCase
不太适合您的JSON密钥,则Swift会允许使用自定义策略。 换句话说,您可以提供用于将CodingKey数组转换为单个CodingKey结果的闭包。
最后但并非最不重要的一点是,如果这些策略都不能将密钥转换为属性名称,那么您始终可以提供自定义的CodingKeys枚举。 简而言之,您想使用最简单,最一致的策略来映射属性名称或JSON键。
自定义属性值
Twitter的JSON数据的另一个怪癖是它们如何格式化某些属性值。 让我们看一下最容易处理的日期。
如何处理日期
默认情况下,日期从2001年1月1 日起被序列化为浮动小数,特别是TimeInterval(秒数)。但是,如果JSON属性值的格式不同,则可以使用以下可用策略之一:
因此, 如果日期格式不遵循上述任何一种格式 , 则需要提供闭包来进行转换,或者提供 DateFormatter 。 在DateFormatter中指定格式的主要方法是使用dateFormat String属性。 换句话说,通过使用Unicode提供的代码,我们可以创建dateFormat字符串来表示Twitter提供的日期和时间的格式。 结果,从JSON中的日期Mon Mar 28 14:39:13 +0000 2016
,我们可以将格式推断为: eee MMM dd HH:mm:ss ZZZZ yyyy
。 因此,我们可以创建一个DateFormatter并相应地设置策略:
let dateFormat = "eee MMM dd HH:mm:ss ZZZZ yyyy"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = dateFormat
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
总之,通过自定义DateFormatter,可以将日期解码为Date属性值。 但是,颜色将更具挑战性。
属性值的自定义解码
对于配置文件背景色之类的颜色,我们需要创建一个单独的结构并自己实现编码和解码,以将十六进制字符串转换为可用的颜色结构:
进口基金会
导入CoreGraphics
公共结构颜色:可编码{
公众让红色:CGFloat
公众让绿:CGFloat
公众让蓝色:CGFloat
公开租赁alpha:CGFloat
public init(来自解码器:Decoder)抛出{
让hexCode =尝试解码器.singleValueContainer()。decode(String.self)
让扫描仪=扫描仪(string:hexCode)
var hexint:UInt32 = 0
扫描仪.scanHexInt32(&hexint)
self.red = CGFloat((hexint&0xff0000)>> 16)/ 255.0
self.green = CGFloat((hexint&0xff00)>> 8)/ 255.0
self.blue = CGFloat((hexint&0xff)>> 0)/ 255.0
self.alpha = 1
}
公共功能编码(对编码器:编码器)抛出{
let string = String(格式:“%02lX%02lX%02lX”,lroundf(Float(红色* 255.0)),lroundf(Float(绿色* 255.0)),lroundf(Float(蓝色* 255.0)))
var容器= encoder.singleValueContainer()
尝试container.encode(string)
}
}
在这种情况下,我们将覆盖init方法,并从singleValueContainer
获取字符串。 之后,我们使用扫描仪将彩色十六进制代码转换为十六进制数字。 然后使用按位和逻辑运算的组合来获取介于0到255之间的每个分量值。最后,我们使用十进制除法来计算介于0到1之间的值。(为完成起见,我也包括了encoding方法。)结果,我们可以轻松地将十六进制代码字符串转换为Color结构。
由于我们无法使用现有的系统颜色类型,因此以下代码可将我们的自定义Color结构转换为系统默认的UI颜色类型:
#if os(iOS)|| 操作系统(watchOS)|| 操作系统(tvOS)
导入UIKit
公共类型别名SystemColor = UIColor
#elseif os(macOS)
公共类型别名SystemColor = NSColor
#万一
扩展名颜色{
public var systemColor:SystemColor {
返回SystemColor(红色:self.red,绿色:self.green,蓝色:self.blue,alpha:self.alpha)
}
}
使我们可以编码
可编码提供了许多可定制性。 但是,在更多情况下,我们希望使用最少的定制量和最多的一致性。
- 对于以类似方式使用的数据,请使用单独的类型,但要实现相同的协议
- 如果属性名称具有一致的策略,请使用KeyEncodingStrategy而不是自定义CodingKeys
- 对于日期,请使用dateDecodingStrategy,DateFormatter或闭包 。
- 对于其他自定义类型,请创建一个单独的类型,然后在此处自定义解码 (和编码)。
如果您还有其他问题要与我联系,请通过Twitter与我联系。 此外,如果您有兴趣了解有关复杂JSON解码或编码以及其他Swift主题的更多信息,请订阅我的时事通讯以保持最新。