[Diving into WWDC 2017] What’s New in Foundation

Foundation 框架的新特性

Or What's New In Swift 4

直达链接:What's New in Foundation

相关主题:

导读

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)之间的映射。

继续学习