用manager封装网络访问

我们把请求DarkSky的代码封装起来,以降低这部分代码在未来对我们App的影响。并为这部分的单元测试,做一些准备工作。

设计DataManager

为了封装DarkSky的请求,我们在Sky中新建一个分组:Manager,并在其中添加一个WeatherDataManager.swif文件。在这里,我们创建一个class WeatherDataManager来管理对DarkSky的请求:

final class WeatherDataManager { }

这里,由于WeatherDataManager不会作为其它类的基类,我们在声明中使用了final关键字,可以提高这个对象的访问性能。

WeatherDataManager有一个属性,表示请求的URL:

final class WeatherDataManager {
    private let baseURL: URL
}

然后,我们用下面的代码创建一个单例,便于我们用一致的方式请求天气数据:

final class WeatherDataManager {
    private let baseURL: URL

    private init(baseURL: URL) {
        self.baseURL = baseURL
    }

    static let shared =
        WeatherDataManager(API.authenticatedUrl)
}

这样,我们就只能通过WeatherDataManager.shared这样的形式,来访问WeatherDataManager对象了。

接下来,我们要在WeatherDataManager中创建一个根据地理位置返回天气信息的方法。由于网络请求是异步的,这个过程只能通过回调函数完成。因此,这个方法看上去应该是这样的:

final class WeatherDataManager {
    // ...
    typealias CompletionHandler =
        (WeatherData?, DataManagerError?) -> Void

    func weatherDataAt(
        latitude: Double,
        longitude: Double,
        completion: @escaping CompletionHandler) {}
}

然后,我们来定义获取数据时的错误:

enum DataManagerError: Error {
    case failedRequest
    case invalidResponse
    case unknown
}

简单起见,我们只定义了三种情况:非法请求、非法返回以及未知错误。然后,我们来实现weatherAt方法,它的逻辑很简单,只是按约定拼接URL,设置HTTP header,然后使用URLSession发起请求就好了:

func weatherDataAt(latitude: Double,
    longitude: Double,
    completion: @escaping CompletionHandler) {
    // 1\. Concatenate the URL
    let url = baseURL.appendingPathComponent("\(latitude), \(longitude)")
    var request = URLRequest(url: url)

    // 2\. Set HTTP header
    request.setValue("application/json",
        forHTTPHeaderField: "Content-Type")
    request.httpMethod = "GET"

    // 3\. Launch the request
    URLSession.shared.dataTask(
        with: request, completionHandler: {
        (data, response, error) in
        // 4\. Get the response here
    }).resume()
}

dataTaskcompletionHandler中,为了让代码看上去干净一些,我们只调用一个帮助函数:

URLSession.shared.dataTask(with: request,
    completionHandler: {
    (data, response, error) in
    DispatchQueue.main.async {
        self.didFinishGettingWeatherData(
            data: data,
            response: response,
            error: error,
            completion: completion)
    }
}).resume()

这里,为了保证可以在dataTask的回调函数中更新UI,我们把它派发到主线程队列执行。完成后,我们来实现didFinishGettingWeatherData

func didFinishGettingWeatherData(
        data: Data?,
        response: URLResponse?,
        error: Error?,
        completion: CompletionHandler) {
        if let _ = error {
            completion(nil, .failedRequest)
        }
        else if let data = data,
            let response = response as? HTTPURLResponse {
            if response.statusCode == 200 {
                do {
                    let weatherData =
                        try JSONDecoder().decode(WeatherData.self, from: data)
                    completion(weatherData, nil)
                }
                catch {
                    completion(nil, .invalidResponse)
                }
            }
            else {
                completion(nil, .failedRequest)
            }
        }
        else {
            completion(nil, .unknown)
        }
    }

其实逻辑很简单,就是根据请求以及服务器的返回值是否可用,把对应的参数传递给了一个可以自定义的回调函数。这样,这个WeatherDataManager就实现好了。

现在,回想起来,我们在这两节中,关于model的部分,已经写了不少的代码了,它们真的能正常工作么?我们如何确定这个事情呢?在把model关联到controller之前,我们最好确定一下。

当然,一个直观的办法就是在类似某个viewDidLoad之类的方法里,写个代码实际请求一下看看。但是估计你也能感觉到这种做法并不地道,如果未来你修改了Manager的代码呢?难道还要重新找个viewDidLoad方法插个空来测试么?估计你自己都不太敢这样做,万一你在恢复的时候不慎修改掉了哪部分功能代码,就很容易随随便便坑上你几个小时。

为此,我们需要一种更专业和安全的方式,来确定局部代码的正确性。这种方式,就是单元测试。在开始测试我们的WeatherDataManager之前,我们要先了解一下Xcode提供的单元测试模板。

了解单元测试模板

首先,在Xcode默认创建的SkyTests分组中,删掉默认的SkyTests.swift。然后在SkyTests Group上点右键,选择New File...

DarkSkyAndModel

其次,在右上角的filter中,输入unit,找到单元测试的模板。选中Unit Test Case Class,点击Next

DarkSkyAndModel

第三,给测试用例起个名字,例如WeatherDataManagerTest。这个名字最好可以直接表达我们要测试的内容。这样,不同的开发者都可以方便的了解到实际测试的内容:

DarkSkyAndModel

第四,接下来,Xcode就会提示我们是否需要创建一个bridge header,由于我们在纯Swift环境中开发,因此,选择Don't Create,并点击Finish按钮。

设置好保存路径之后,我们就可以在SkyTests分组中,找到新添加的测试用例了。在开始编写测试之前,这个文件中有几个值得说明的地方:

首先,在文件一开始,要添加下面的代码引入项目的main module。这样,才能在测试用例中,访问到项目定义的类型:

import XCTest
@testable import Sky

其次,在生成的代码中,WeatherDataManagerTest派生自XCTestCase,表示这是一个测试用例。

第三,在WeatherDataManagerTest里,我们可以把所有的测试前要准备的代码,写在setUp方法里,而把测试后需要清理的代码,写在tearDown方法里。这里要注意下面代码中注释的位置,初始化代码写在super.setUp()后面,清理代码要写在super.tearDown()前面:

class WeatherDataManagerTest: XCTestCase {

    override func setUp() {
        super.setUp()
        // Your set up code here...
    }

    override func tearDown() {
        // Your tear down code here...
        super.tearDown()
    }

    // ...
}

第四,Xcode为我们生成了两个默认的测试方法:

class WeatherDataManagerTest: XCTestCase {
    func testExample() {
        // ...
    }

    func testPerformanceExample() {
        // ...
    }
}

要注意的是,所有测试方法都必须用test开头,Xcode才会识别它们并自动执行。这里,可以先把它们删掉,稍后我们会编写自己的测试方法。

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

推荐阅读更多精彩内容