原文來自于 objc.io
Transcript
0:01 我們來討論下 Swift talk app 的網(wǎng)絡(luò)層邻吞。我們認(rèn)為這是個有趣的例子因為設(shè)計與之前的 Objective-C 項目不同曾我。通常箕昭,我們將創(chuàng)建一個有初始化方法的Webservice
類來呼叫一個特定的 endpoints 栅炒。這些方法返回從 endpoints 通過一個回調(diào)函數(shù)獲得的數(shù)據(jù)秫筏。舉個例子种冬,我們可以有個網(wǎng)絡(luò)請求的loadEpisodes
方法付燥,分析結(jié)果敷钾,初始化一些 Episode
對象枝哄,并返回一個包含Episode
的數(shù)組。我們同樣可以有一個loadMedia
方法阻荒,通過同樣的步驟來夾在一個特定 episode 的 media:
final class Webservice {
func loadEpisodes(completion: ([Episode]?) -> ()) {
// TODO
}
func loadMedia(episode: Episode, completion: (Media?) -> ()) {
// TODO
}
}
final
可以用來修飾 class挠锥,func 或者 var ,修飾過后的內(nèi)容不允許被重寫或者繼承侨赡。
0:50 在 Objective-C 中蓖租,這個方式的優(yōu)點是回調(diào)結(jié)果有個正確的類型。舉個例子羊壹,我們將獲得一個 episodes 的數(shù)組而不僅僅是個id
類型蓖宦,因為這是一個從網(wǎng)絡(luò)加載任何數(shù)據(jù)的方法。這個方式的優(yōu)點是每個方法在幕后執(zhí)行一個復(fù)雜任務(wù):網(wǎng)絡(luò)請求油猫,分析數(shù)據(jù)稠茂,初始化一些 model 對象,最后通過回調(diào)返回他們情妖。這里有很多地方會出錯睬关,正因為如何,調(diào)試是很難的毡证。因為這些方法還是異步的电爹,所以讓他們更難調(diào)試。此外情竹,我們需要一個網(wǎng)絡(luò)棧設(shè)置或者模擬,這也使調(diào)試更復(fù)雜。在 Swift 中秦效,有其他的方式來讓這事簡單化雏蛮。
The Resource Struct
1:51 我們創(chuàng)建一個Resource
結(jié)構(gòu)體,這是一個泛型類型阱州。這個結(jié)構(gòu)體有2個屬性:endpoint 的 URL和parse
函數(shù)挑秉。parse
函數(shù)試圖將一些數(shù)據(jù)轉(zhuǎn)化為結(jié)果:
struct Resource<A> {
let url: NSURL
let parse: NSData -> A?
}
2:12 parse
函數(shù)的返回類型是可選的因為分析可能失敗。代替可選值苔货,我們也可以使用Result
類型或者使他拋出詳細(xì)的錯誤信息犀概。此外,如果我們只想處理 JSON夜惭,parse
函數(shù)可以使用AnyObject
來代替NSData
姻灶。然而,使用AnyObject
會阻止我們使用我們的Resource
除了 JSON - 例如圖片诈茧。
2:59 現(xiàn)在創(chuàng)建episodesResource
产喉。這只是一個返回NSData
的簡單 resource:
let episodesResource = Resource<NSData>(url: url, parse: { data in
return data
})
3:33 最后,這個 resource 應(yīng)該有一個[Episode]
的 result 類型敢会。我們將重構(gòu)parse
函數(shù)通過幾個步驟將NSData
的 result 改成[Episode]
的 result 類型曾沈。
The Webservice Class
3:58 從網(wǎng)上加載資源,我們創(chuàng)建一個Webservice
類鸥昏,他只有一個方法:load
塞俱。這個方法是通用的,并將 resource 作為第一個參數(shù)吏垮。這二個參數(shù)是個閉包障涯,使用 A?
是因為請求有可能失敗或者某些東西會出錯。在load
方法里惫皱,我們使用NSURLSession.sharedSession()
來做請求像樊。我們創(chuàng)建一個 data task 用從 resource 中獲得的 URL。resource 捆綁了我們需要的所有做請求的信息旅敷。目前生棍,只包含了 URL,但在將來會有更多的屬性媳谁。在 data task 的回調(diào)里涂滴,我們使用 data 作為第一個參數(shù)。我們忽略其他2個參數(shù)晴音。最后柔纵,開始 data task,我們調(diào)用resume
:
final class Webservice {
func load<A>(resource: Resource<A>, completion: (A?) -> ()) {
NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
if let data = data {
completion(resource.parse(data))
} else {
completion(nil)
}
}.resume()
}
}
5:38 調(diào)用閉包锤躁,我們不得不通過parse
函數(shù)來將 data 轉(zhuǎn)為資源的結(jié)果類型搁料。因為 data 是可選的,我們使用可選綁定。如果 data 是nil
,我們調(diào)用閉包使用nil
郭计。如果 data 不是nil
,我們調(diào)用閉包使用parse
函數(shù)霸琴。
6:22 因為我們運行在 playground,我們必須讓他一直執(zhí)行下去,否則昭伸,主線程完成就會停止:
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
7:00 我們創(chuàng)建一個Webservice
實例然后調(diào)用load
方法和episodesResource
一起梧乘。在閉包里,我們輸出 result:
Webservice().load(episodesResource) { result in
print(result)
}
7: 18 在控制臺中庐杨,我們可以看到一些原始的二進(jìn)制數(shù)據(jù)选调。在我們繼續(xù)之前,我們將重構(gòu)load
方法--我們不喜歡調(diào)用2次completion
灵份。我們嘗試使用guard let
仁堪。然而,我們還是調(diào)用了2次completion
各吨,還添加了返回語句:
final class Webservice {
func load<A>(resource: Resource<A>, completion: (A?) -> ()) {
NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
guard let data = data else {
completion(nil)
return
}
completion(resource.parse(data))
}.resume()
}
}
8:07 使用flatMap
是其他的辦法枝笨。首先,我們嘗試map
揭蜒。然而横浑,map
給我們了一個A??
代替A?
。使用flatMap
將移除2個可選:
final class Webservice {
func load<A>(resource: Resource<A>, completion: (A?) -> ()) {
NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
let result = data.flatMap(resource.parse)
completion(result)
}.resume()
}
}
flatMap
可以去掉空值
Parsing JSON
8:58 下一步我們改變episodesResource
為了將NSData
解析為 JSON 對象屉更。我們使用內(nèi)置的 JSON 解析徙融。因為 JSON 解析會 throwing operation,我們使用try?
來調(diào)用 parsing 方法:
let episodesResource = Resource<AnyObject>(url: url, parse: { data in
let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
return json
})
9:40 在側(cè)邊欄瑰谜,我們可以看到二進(jìn)制數(shù)據(jù)被解析欺冀。這是個字典數(shù)組,所以我們可以讓結(jié)果類型更加明確萨脑。JSON 字典包含一個 String
的 key 和AnyObject
的 values:
typealias JSONDictionary = [String: AnyObject]
let episodesResource = Resource<[JSONDictionary]>(url: url, parse: { data in
let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
return json as? [JSONDictionary]
})
10:23 下一步是返回一個Episode
數(shù)組隐轩,所以我們需要將 JSON 字典轉(zhuǎn)化到Episode
里。在初始化之前渤早,我們添加一些屬性到Episode
里:id
和title
职车,都是String
。在真實的項目里鹊杖,這里有更多的屬性:
struct Episode {
let id: String
let title: String
// ...
}
11:13 我們現(xiàn)在在 extension 里寫個可失敗構(gòu)造器悴灵。在這個 extension 里,我們保留了默認(rèn)的成員逐一初始化骂蓖。在這個構(gòu)造器里积瞒,我們首先需要檢查字典是否包含我們需要的數(shù)據(jù)。我們使用guard
來做這件事登下,然后我們檢查字典里的 id
是否是Srting
類型茫孔,取出title
做相同的操作叮喳。如果 guard 失敗,我們馬上返回nil
缰贝。如果成功嘲更,我們給 id
和title
賦值:
extension Episode {
init?(dictionary: JSONDictionary) {
guard let id = dictionary["id"] as? String,
title = dictionary["title"] as? String else { return nil }
self.id = id
self.title = title
}
}
12:48 我們現(xiàn)在重構(gòu)episodesResource
來返回一個Episode
數(shù)組。首先揩瞪,我們檢查我們是否有個 JSON 字典。否則篓冲,我們馬上返回nil
李破。字典轉(zhuǎn)化為 episodes,我們可以使用map
并使用可失敗Episode.init
作為我們的轉(zhuǎn)換函數(shù)。然而壹将,構(gòu)造器返回可選值嗤攻,所以使用map
結(jié)果是[Episode?]
。但是我們不想在這里返回nil
,應(yīng)該是[Episode]
诽俯。我們使用flatMap
來修復(fù)這個問題妇菱。
14:18 在我們的項目里,flatMap
的不同版本暴区。flatMap
會默認(rèn)忽略不能解析的字典闯团,我們想一旦字典無效就完全失敗:
extension SequenceType {
public func failingFlatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]? {
var result: [T] = []
for element in self {
guard let transformed = try transform(element) else { return nil }
result.append(transformed)
}
return result
}
}
14:52 我們可以重構(gòu)我們的parse
函數(shù)來移除2個return
仙粱。首先房交,我們嘗試使用guard
,但是這個不能移除2個return
伐割。然而候味,guard
可以讓我們擺脫嵌套:
let episodesResource = Resource<[Episode]>(url: url, parse: { data in
let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
guard let dictionaries = json as? [JSONDictionary] else { return nil }
return dictionaries.flatMap(Episode.init)
})
15:28 我們嘗試在dictionaries
里使用 optional chaining來去除2次return
:
let episodesResource = Resource<[Episode]>(url: url, parse: { data in
let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
let dictionaries = json as? [JSONDictionary]
return dictionaries?.flatMap(Episode.init)
})
15:44 這開始變得難以理解。我們有一個可選的dictionaries
然后我們使用 optional chaining 來調(diào)用flatMap
,將可失敗構(gòu)造器作為參數(shù)隔心。在這里白群,我們也許會用guard
的版本,那個更加清晰硬霍。
JSON Resources
16:07 一旦我們創(chuàng)建更多的 resources帜慢,必須復(fù)制 JSON 解析到每個 resources。移除這個復(fù)制须尚,我們可以創(chuàng)建一個不同的 resources崖堤。然而,我們可以擴展現(xiàn)存的 resources 通過其他的構(gòu)造器耐床。這個構(gòu)造器頁使用 URL密幔,但是 parse 函數(shù)類型是AnyObject -> A?
。我們在包裹了這個 parse 函數(shù)在其他的NSData -> A?
函數(shù)類型里并在這個閉包里從episodesResource
里移除了 JSON 解析撩轰。因為解析 JSON 是可選的胯甩,我們可以使用flatMap
來調(diào)用parseJSON
:
extension Resource {
init(url: NSURL, parseJSON: AnyObject -> A?) {
self.url = url
self.parse = { data in
let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
return json.flatMap(parseJSON)
}
}
}
18:00 現(xiàn)在我們可以使用新的構(gòu)造器來改變我們的episodesResource
:
let episodesResource = Resource<[Episode]>(url: url, parseJSON: { json in
guard let dictionaries = json as? [JSONDictionary] else { return nil }
return dictionaries.flatMap(Episode.init)
})
Naming the Resources
18:17 另外一件我們不喜歡的事情是episodesResource
在公共的命名空間昧廷。我們也不喜歡他的命名。我們可以將episodesResource
移到Episode
的擴展里作為一個類屬性偎箫。我們將他重命名為allEpisodesResource
木柬。然而,我們還是不怎么喜歡這個名字淹办∶颊恚看看這個類型,很清楚的表明它屬于Episode
怜森。從類型里也可以明白是一個 resource速挑,所以我們?yōu)槭裁床粌H僅命名為call
?:
Webservice().load(Episode.all) { result in
print(result)
}
19:40 其實這是個危險的命名副硅,也許你會和集合混淆姥宝。雖然我們不認(rèn)為這是個問題,因為你試圖使用集合會立即失敗恐疲。
20:09 在Episode
擴展中腊满,我們也可以添加其他依賴于 episode 的屬性的resources——例如,一個media
resource培己,從指定的 episode 中獲得 media碳蛋。在media
resource 中,我們可以使用字符串插入來組成 URL:
extension Episode {
var media: Resource<Media> {
let url = NSURL(string: "http://localhost:8000/episodes/\(id).json")!
// TODO Return the resource ...
}
}
21:18 如果我們在Episode
結(jié)構(gòu)體中需要更多的參數(shù)是無效的省咨,我們可以改變 resource 屬性作為一個方法然后直接傳遞參數(shù)疮蹦。
21:27 我們喜歡這個網(wǎng)絡(luò)請求的方式因為幾乎所有的代碼都是同步的。這很簡單茸炒,很容易調(diào)試愕乎,而且我們也不需要設(shè)置網(wǎng)絡(luò)棧或者調(diào)試一些東西壁公。唯一異步的代碼是Webservice.load
方法感论。這個架構(gòu)是個不錯的例子對于 Swift 來說;Swift 的泛型和結(jié)構(gòu)體讓這樣設(shè)計變得很簡單紊册。同樣的事情在 OC 里是做不了的比肄。
22:21 讓我們添加POST
支持在下一節(jié)。