用manager封装网络访问

2023-12-21  本文已影响0人  醉看红尘这场梦

我们把请求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才会识别它们并自动执行。这里,可以先把它们删掉,稍后我们会编写自己的测试方法。

上一篇下一篇

猜你喜欢

热点阅读