译 Swift Talk -- NetWorking

原文来自于 objc.io

Transcript

0:01 我们来讨论下 Swift talk app 的网络层。我们认为这是个有趣的例子因为设计与之前的 Objective-C 项目不同。通常,我们将创建一个有初始化方法的Webservice类来呼叫一个特定的 endpoints 。这些方法返回从 endpoints 通过一个回调函数获得的数据。举个例子,我们可以有个网络请求的loadEpisodes方法,分析结果,初始化一些 Episode对象,并返回一个包含Episode的数组。我们同样可以有一个loadMedia方法,通过同样的步骤来夹在一个特定 episode 的 media:

final class Webservice {
    func loadEpisodes(completion: ([Episode]?) -> ()) {
        // TODO
    }

    func loadMedia(episode: Episode, completion: (Media?) -> ()) {
        // TODO
    }
}

final可以用来修饰 class,func 或者 var ,修饰过后的内容不允许被重写或者继承。

0:50 在 Objective-C 中,这个方式的优点是回调结果有个正确的类型。举个例子,我们将获得一个 episodes 的数组而不仅仅是个id类型,因为这是一个从网络加载任何数据的方法。这个方式的优点是每个方法在幕后执行一个复杂任务:网络请求,分析数据,初始化一些 model 对象,最后通过回调返回他们。这里有很多地方会出错,正因为如何,调试是很难的。因为这些方法还是异步的,所以让他们更难调试。此外,我们需要一个网络栈设置或者模拟,这也使调试更复杂。在 Swift 中,有其他的方式来让这事简单化。

The Resource Struct

1:51 我们创建一个Resource结构体,这是一个泛型类型。这个结构体有2个属性:endpoint 的 URL和parse函数。parse函数试图将一些数据转化为结果:

struct Resource<A> {
    let url: NSURL
    let parse: NSData -> A?
}

2:12 parse函数的返回类型是可选的因为分析可能失败。代替可选值,我们也可以使用Result类型或者使他抛出详细的错误信息。此外,如果我们只想处理 JSON,parse函数可以使用AnyObject来代替NSData。然而,使用AnyObject会阻止我们使用我们的Resource除了 JSON - 例如图片。

2:59 现在创建episodesResource。这只是一个返回NSData的简单 resource:

let episodesResource = Resource<NSData>(url: url, parse: { data in
    return data
})

3:33 最后,这个 resource 应该有一个[Episode]的 result 类型。我们将重构parse函数通过几个步骤将NSData的 result 改成[Episode]的 result 类型。

The Webservice Class

3:58 从网上加载资源,我们创建一个Webservice类,他只有一个方法:load。这个方法是通用的,并将 resource 作为第一个参数。这二个参数是个闭包,使用 A?是因为请求有可能失败或者某些东西会出错。在load方法里,我们使用NSURLSession.sharedSession()来做请求。我们创建一个 data task 用从 resource 中获得的 URL。resource 捆绑了我们需要的所有做请求的信息。目前,只包含了 URL,但在将来会有更多的属性。在 data task 的回调里,我们使用 data 作为第一个参数。我们忽略其他2个参数。最后,开始 data task,我们调用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 调用闭包,我们不得不通过parse函数来将 data 转为资源的结果类型。因为 data 是可选的,我们使用可选绑定。如果 data 是nil,我们调用闭包使用nil。如果 data 不是nil,我们调用闭包使用parse函数。

6:22 因为我们运行在 playground,我们必须让他一直执行下去,否则,主线程完成就会停止:

import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

7:00 我们创建一个Webservice实例然后调用load方法和episodesResource一起。在闭包里,我们输出 result:

Webservice().load(episodesResource) { result in
    print(result)
}

7: 18 在控制台中,我们可以看到一些原始的二进制数据。在我们继续之前,我们将重构load方法--我们不喜欢调用2次completion。我们尝试使用guard let。然而,我们还是调用了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 对象。我们使用内置的 JSON 解析。因为 JSON 解析会 throwing operation,我们使用try?来调用 parsing 方法:

let episodesResource = Resource<AnyObject>(url: url, parse: { data in
    let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
    return json
})

9:40 在侧边栏,我们可以看到二进制数据被解析。这是个字典数组,所以我们可以让结果类型更加明确。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数组,所以我们需要将 JSON 字典转化到Episode里。在初始化之前,我们添加一些属性到Episode里:idtitle,都是String。在真实的项目里,这里有更多的属性:

struct Episode {
    let id: String
    let title: String
    // ...
}

11:13 我们现在在 extension 里写个可失败构造器。在这个 extension 里,我们保留了默认的成员逐一初始化。在这个构造器里,我们首先需要检查字典是否包含我们需要的数据。我们使用guard来做这件事,然后我们检查字典里的 id是否是Srting类型,取出title做相同的操作。如果 guard 失败,我们马上返回nil。如果成功,我们给 idtitle赋值:

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 我们现在重构episodesResource来返回一个Episode数组。首先,我们检查我们是否有个 JSON 字典。否则,我们马上返回nil。字典转化为 episodes,我们可以使用map并使用可失败Episode.init作为我们的转换函数。然而,构造器返回可选值,所以使用map结果是[Episode?]。但是我们不想在这里返回nil,应该是[Episode]。我们使用flatMap来修复这个问题。

12:48 code

14:18 在我们的项目里,flatMap的不同版本。flatMap会默认忽略不能解析的字典,我们想一旦字典无效就完全失败:

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 我们可以重构我们的parse函数来移除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 来调用flatMap,将可失败构造器作为参数。在这里,我们也许会用guard的版本,那个更加清晰。

JSON Resources

16:07 一旦我们创建更多的 resources,必须复制 JSON 解析到每个 resources。移除这个复制,我们可以创建一个不同的 resources。然而,我们可以扩展现存的 resources 通过其他的构造器。这个构造器页使用 URL,但是 parse 函数类型是AnyObject -> A?。我们在包裹了这个 parse 函数在其他的NSData -> A?函数类型里并在这个闭包里从episodesResource里移除了 JSON 解析。因为解析 JSON 是可选的,我们可以使用flatMap来调用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 现在我们可以使用新的构造器来改变我们的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,所以我们为什么不仅仅命名为call?:

18:17 code
Webservice().load(Episode.all) { result in
    print(result)
}

19:40 其实这是个危险的命名,也许你会和集合混淆。虽然我们不认为这是个问题,因为你试图使用集合会立即失败。

20:09 在Episode扩展中,我们也可以添加其他依赖于 episode 的属性的resources——例如,一个mediaresource,从指定的 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结构体中需要更多的参数是无效的,我们可以改变 resource 属性作为一个方法然后直接传递参数。

21:27 我们喜欢这个网络请求的方式因为几乎所有的代码都是同步的。这很简单,很容易调试,而且我们也不需要设置网络栈或者调试一些东西。唯一异步的代码是Webservice.load方法。这个架构是个不错的例子对于 Swift 来说;Swift 的泛型和结构体让这样设计变得很简单。同样的事情在 OC 里是做不了的。

22:21 让我们添加POST支持在下一节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容