Foundation 框架的新特性
Or What’s New In Swift 4
相关主题:
导读
Foundation 是什么? Foundation 作为基础组件,提供了跨 macOS、iOS、watchOS 和 tvOS SDKs 平台的类型组件, 包含数据存储,字符处理,时间计算,排序,查找,网络等功能。大家熟悉的 NSURL、NSString、NSDictionary 类型都是 Foundation 的一部分。
本文将介绍 Foundation 在 WWDC 2017 大会中新增加地两个实用的特性。这两个新特性包含在 Swift 4 中,如果文章中没有特殊说明,那么默认是 Swift 4 环境。
本文主要介绍两个主题,一个是 Swift 中 KVC 和 KVO 重新设计;另一个是协议 Codable 解决强类型和松散的数据格式的映射问题。
Key Paths and Key Value Observation
Swift 3.0 Key Path
简单回顾一下在 Swift3.0 时期使用 KeyPath 的感受,没有类型推导,IDE 支持差,KVO 是个高级技能,不小心会奔溃。
// Swift 3 String key Path
@objcMembers class Kid : NSObject {
dynamic var nickname: String = ""
dynamic var age: Double = 0.0
dynamic var bestFriend: Kid? = nil
dynamic var firends:[Kid] = []
}
var ben = Kid(nickName: "Benji", age: 5.5)
let kidsNamekeyPath = #keyPath(Kid.nickname) // String, 没有携带属性的类型信息
// 使用key value coding 进行实例ben的属性的读写
let name = ben.valueForKeyPath(kidsNameKeyPath) // valueForKeyPath(_: String) -> Any
ben.setValue("Ben", forKeyPath: kidsNameKeyPath) // setValue(_, forKeyPath: String) -> Any
初识 Swift 4.0 keyPath
Swift 是强类型语言,强类型语言,强类型语言,在 keyPath 实现上可以走的更远更优雅。Swift 中的 property 是类型安全的(type-safe),可以推导出很多有用的东西。Swift 世界里 keyPath 应该是这样地:
- 可以进行 Property 遍历,具有友好的 IDE 提示
- 静态类型安全
- 处理效率高
- 适用于 Swift 中所有类型 -> Class、Struct
- 是 Swift 语言特性,在任何可以执行 Swift 的地方都适用
SE-0161 中介绍的 Smart Key Path 使用简单,安全。 现在 KeyPath 是一种抽象数据类型。
// getter
let age = ben[keypPath: \Kid.age]
/*
let nickname = ben[keypPath: \Kid.nickname]
let nickname = ben[keypPath: \.nickname] //缩写
let characters = ben[keypPath: \Kid.nickname.characters] //链式
let firstfriend = ben[keypPath: \kid.friends[0]] //下标取值
*/
// setter
ben[keypath: \kid.nickName] = "Ben"
Swift 中各种数据类型具有统一的 KeyPath 语法:
Type Properties / Subscripts
-------------------------------------------------------
struct let/var
class get/set
@objc class stored or computed
KeyPath 的基本用法
//Using Swift 4 KeyPaths
struct BirthdayParty {
let celebrant: Kid
var theme: String
var attending: [Kid]
}
let bensParty = BirthdayParty(celebrant: ben, theme: "COnstruction", attending: [])
//let birthdayKid = bensParty[keypath: \BirthdayParty.celebrant]
let birthdayKid = bensParty[keypath: \.celebrant]
//bensParty[keypath: \BirthdayParty.theme] = "Pirate"
bensParty[keypath: \.theme] = "Pirate"
keyPath 和 Property 的关系
let nicknameKeyPath = \Kid.nickname // keyPath<Kid, String>
let birthdayKidsAgeKeyPath = \Birthdayparty.celebrant.age // KeyPath<BirthdayPath, Double>
let birthdayBoysAge = bensParty[keypath: birthdayKidsAgeKeyPath] // Double
let mia = Kid(nickname: "Mia", age: 4.5)
let miasParty = BirthdayParty(celebrant: mia, theme: "Space", attending: [])
let birthdayGirlsAge = miasParty[keyPath: birthdayKidsAgeKeyPath] // Double
拼接 keyPath
// keyPath作为强类型,有自己的方法appending
func partyPersonsAge(party: Birthdayparty, participantPath: Keypath<BirthdayParty, Kid>) -> Double {
let kidsAgeKeyPath = participantPath.appending(\.age)
return party[keyPath: kidsAgeKeyPath]
}
let birthdayBoysAge = partyPersonsAge(bensparty, \.celebrant)
let firstAttendeesAge = partyPersonsAge(bensparty, \.attendees[0])
分析 \BirthdayParty.celebrant.appending(\Kid.age) 拼接有什么样的规则,和类型属性推导联系在一起就很好的理解了。
Type Erased key paths
// Type Erased Key Paths
let titles = ["heme", "Attending", "Birthday Kid"]
let partypaths = [\BirthdayParty.theme, \BirthdayParty.attending, \Birthdayparty.celebrant]
// [PartialKeyPath<BirthdayParty>] 拥有共同的 base Type BirthdayParty
for (title, partyPath) in zip(titles, partypath) {
let partyValue = miasParty[keyPath: partypath]
print("\(title) \n \(partyValue)\n")
}
Mutating key Paths
// Mutating key Paths
extension BirthdayParty {
//func blowCandles(agekeyPath: WritableKeyPath<BirthdayParty, Double> { // complie error
mutating func blowCandles(agekeyPath: WritableKeyPath<BirthdayParty, Double> {
let age = self[keypath: ageKeyPath]
self[keyPath: ageKeyPath] = floor(age) + 1.0 // 如何才能正确运行
}
}
bensParty. blowCandles(ageKeyPath: \.celebrant.age)
BirthdayPary 是 struct Value 类型,WritableKeyPath 直接使用在可变的值类型赋值(inout/mutating)
extension BirthdayParty {
func blowCandles(agekeyPath: ReferenceWritableKeyPath<BirthdayParty, Double> {
let age = self[keypath: ageKeyPath]
self[keyPath: ageKeyPath] = floor(age) + 1.0
}
}
bensParty. blowCandles(ageKeyPath: \.celebrant.age)
BirthdayParty.celebrant 是 Class reference 类型,ReferenceWritableKeyPath 对引用类型赋值
KeyPath 继承关系链
AnyKeyPath
PartialKeyPath<Base>
keyPath<Base, Property>
WritableKeyPath<Base, Property>
ReferenceWritableKeyPath<Base, Property>
获取属性时其实是返回 KeyPath:
Property KeyPath
----------------------------------
readonly property keyPath
readwrite property WritableKeyPath / ReferenceWritableKeyPath
key Paths Capture By Value
var index = 0
let whichKidkeyPath = \BirthdatParty.attendees[index] // capture \BirthdayParty.attendees[0]
let firstAttendeesAge = partyPersonsAge(party, whichKidkeyPath)
index = 1
let sameAge = partyPersonsAge(party, whichKidkeyPath)
//let sameAge = partyPersonsAge(party, \BirthdatParty.attendees[0])
Key Value Observing
let observation = mia.observe(\.age) { observed, change in
}
- observation 是 Observation Token, 赋值 nil 释放监听, 管理 Observation 的生命周期
- observed 就是 mia, Kid 类型
- change NSKeyValueObservedChange\
最后
为了兼容以往的 API,#keyPath(Kid.nickname) 会继续保留,是不是很想实践一下更加安全更高效率的 KeyPath。
Encoding and Decoding
自定义数据类型和归档数据格式(JSON, plist)的转化,解决强类型数据格式和松散数据格式的鸿沟。JSON松散数据结构,没有数据类型系统。一下正式介绍 Apple Codable 模型序列化工具。
Decoding
来个简单的例子
let jsonData = """
{
"name" : "Monalisa Octocat",
"email" : "support@github.com",
"date" : "2011-04-14T16:00:49Z"
}
""".data(using: .utf8)
struct Author: Codable {
let name : String
let email : String
let date : Date
}
let decoder = JSONDecoder() // JOSN 格式解码器 json -> swift struct/class
decoder.dataDecodingStagegy = .iso8601 // 设置如何解析 时间戳格式
let author = try decoder.decode(Author.self, form: jsonData)
来个嵌套的例子
let jsonData = """
{
"url" : "https://api.github.com/.../6dcb09",
"author" : {
"name" : "Monalisa Octocat",
"email" : "support@github.com",
"date" : "2011-04-14T16:00:49Z"
},
"message" : "Fix all the bugs",
"comment_count" : 0
}
""".data(using: .utf8)
struct Commit : Codable {
let url : URL //URL 实现Codable协议
struct Author: Codable {
let name : String
let email : String
let date : Date
}
let author : Author
let message : String
let comment_count: Int
}
let decoder = JSONDecoder()
decoder.dataDecodingStagegy = .iso8601
let commit = try decoder.decode(Commit, form: jsonData)
let commitDate = commit.author.date
Foundation 中大部分数据类型都实现了 Codable 协议,所以可以随意组合。看看都有谁实现 Codable:
CGFloat IndexPath
AffineTransform IndexSet
Calendar Locale
CharacterSet Measurement
Data NSRange
Date PersonNameComponents
DateComponents TimeZone
Dateinterval URL
Decimal UUID
Coding Protocols
其实 Codable 协议是由 Encodable 协议和 Decodable 协议叠加而成。
typealias Codable = Encodable & Decodable
public protocol Encodable {
func encode(to encoder: Encoder) throws
}
public protocol Decodable {
init(from decoder: Decoder) throws
}
Swift protocol extension 可以为接口提供默认实现,下面的例子展示了,编译器自动产生代码,揭示了 Codable 实现的原理。 基础类型都已经实现了 Codable 协议,所以只需要确定自定义类型是否实现了 Codable 协议。
struct Commit : Codable {
let url : URL,
struct Author: Codable {
let name : String
let email : String
let date : Date
}
let author : Author
let message : String // 此处 String? 可以表示 message 是一个可选字段
let commentCount: Int // 如果 CodingKeys 缺少 comment_count,那么在 init encode 中忽略,但是此处应该有默认值
private enum CodingKeys : String, CodingKey {
case url
case message
case author
case commentCount = "comment_count" //用于完成自定义属性映射
}
/* Complier Generated */
// Encodable
public func encode(to encoder: Encoder) throws {
}
// Decodable
init(from decoder: Decoder) throws {
}
/* end */
}
CodingKeys 简单理解为属性字段的 keyMaper, 默认的行为是一一对应, 如果不是对应的可以使用 commentCount = “comment_count” 进行自定义映射。 如果某个字段是可选的,那么将该字段设置为 Optional 类型。
Catch Error
程序尽量不要奔溃,挽救尽可能多的错误,面对可能出现的问题:
- Encoding
- Invalid value
- Decoding
- Type mismatch
- Missage key
- Missing value
- Data corrupt
do {
commit = try decoder.decode([GitHubCommit].self, form: data)
} catch DecodingError.keyNotFound(let key, let context) {
print("Missing key :\(key)")
} catch DecodingError.valueNotFound(let type , let context) {
// ...
} catch {
// ..
}
高级解析 Decoding & Encodeing
struct Commit: Codable {
struct Autor: Codable { /* .. */}
let url : URL
let message: String
let author : Author
let commentCount: Int
private enum CodingKeys : String , CodingKey { /* ..*/}
public init(form decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// container管理Key<->Value的映射
url = try container.decode(URL.self, forKey:.url)
// 通过 container 获取特定属性的值,获取值过程可以是递归地,或者依据某种策略地
guard url.scheme == "https" else {
throws DecodingError.dataCorrupted(DecodingError.context(codingPath: container.codingPath + [CodingKeys.url], debugDescription: "URLs require https"))
}
message = try container.decode(String.self, forKey: .message)
author = try container.decode(Author.self, forKey: .author)
commentCount = try container.decode(Int.self, forKey: .commentCount)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(url, forKey: .url)
try container.encode(message, forKey: .message)
try container.encode(author, forKey: .author)
try container.encode(commentCount, forKey: .commentCount)
}
}
public protocol CodingKey {
var stringValue: String {get}
var intValue: Int? {get}
init?(stringValue: String)
init?(intValue: Int)
}
CodingKey 具有两个属性和两个构造函数,case commentCount = “commentcount” 表示 stringValue = “commentcount”, intValue = nil; case url 表示 stringValue = url, intValue = nil。 也可以指定 JSON key 是 int 类型,例子: case url = 10。
container 可以认为 value 的映射器,基于某种关系进行映射。提供了以下几种 container:
- keyed containers 需要和 CodingKey Protocol 配合使用, CodingKey 标识字段映射关系
- Unkeyed Containers 根据元素的位置进行编码
- Single Value COntainers
- Nested Containers
struct Point2D : Encodable {
var x: Double
var y: Double
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(x)
try container.encode(y)
}
}
// [1.5, 2.9]
上面举了很多的例子,相信大家已经可以动手写 Model 映射。Codable 协议目前只支持 JSON 和 plist 文件格式映射,当然可以自定义实现任意格式的映射,Swift 是开源的,可以查看标准库的实现方式。
总结
本次 Foundation 框架又添加了许多新的 API,并且对部分 API 提高的了性能, 特别是 Objective-C 和 Swift 之间的桥接。Swift 作为强类型语言,引入了强类型的 KeyPath,和新的 KVO API, Swift 中的 KVO 操作将会比 Objective-C 中更加的安全。 添加了 Codable 协议,用于解决 Swift 强数据类型和松散数据格式(JSON、plist)之间的映射。