美文网首页
用manager封装网络访问

用manager封装网络访问

作者: 醉看红尘这场梦 | 来源:发表于2023-12-21 16:56 被阅读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才会识别它们并自动执行。这里,可以先把它们删掉,稍后我们会编写自己的测试方法。

    相关文章

      网友评论

          本文标题:用manager封装网络访问

          本文链接:https://www.haomeiwen.com/subject/zhnfshtx.html