用manager封裝網(wǎng)絡(luò)訪問(wèn)

我們把請(qǐng)求DarkSky的代碼封裝起來(lái),以降低這部分代碼在未來(lái)對(duì)我們App的影響。并為這部分的單元測(cè)試,做一些準(zhǔn)備工作。

設(shè)計(jì)DataManager

為了封裝DarkSky的請(qǐng)求,我們?cè)赟ky中新建一個(gè)分組:Manager,并在其中添加一個(gè)WeatherDataManager.swif文件。在這里,我們創(chuàng)建一個(gè)class WeatherDataManager來(lái)管理對(duì)DarkSky的請(qǐng)求:

final class WeatherDataManager { }

這里,由于WeatherDataManager不會(huì)作為其它類的基類,我們?cè)诼暶髦惺褂昧?code>final關(guān)鍵字,可以提高這個(gè)對(duì)象的訪問(wèn)性能。

WeatherDataManager有一個(gè)屬性,表示請(qǐng)求的URL:

final class WeatherDataManager {
    private let baseURL: URL
}

然后,我們用下面的代碼創(chuàng)建一個(gè)單例,便于我們用一致的方式請(qǐng)求天氣數(shù)據(jù):

final class WeatherDataManager {
    private let baseURL: URL

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

    static let shared =
        WeatherDataManager(API.authenticatedUrl)
}

這樣,我們就只能通過(guò)WeatherDataManager.shared這樣的形式,來(lái)訪問(wèn)WeatherDataManager對(duì)象了。

接下來(lái),我們要在WeatherDataManager中創(chuàng)建一個(gè)根據(jù)地理位置返回天氣信息的方法。由于網(wǎng)絡(luò)請(qǐng)求是異步的,這個(gè)過(guò)程只能通過(guò)回調(diào)函數(shù)完成。因此,這個(gè)方法看上去應(yīng)該是這樣的:

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

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

然后,我們來(lái)定義獲取數(shù)據(jù)時(shí)的錯(cuò)誤:

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

簡(jiǎn)單起見(jiàn),我們只定義了三種情況:非法請(qǐng)求、非法返回以及未知錯(cuò)誤。然后,我們來(lái)實(shí)現(xiàn)weatherAt方法,它的邏輯很簡(jiǎn)單,只是按約定拼接URL,設(shè)置HTTP header,然后使用URLSession發(fā)起請(qǐng)求就好了:

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中,為了讓代碼看上去干凈一些,我們只調(diào)用一個(gè)幫助函數(shù):

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的回調(diào)函數(shù)中更新UI,我們把它派發(fā)到主線程隊(duì)列執(zhí)行。完成后,我們來(lái)實(shí)現(xiàn)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)
        }
    }

其實(shí)邏輯很簡(jiǎn)單,就是根據(jù)請(qǐng)求以及服務(wù)器的返回值是否可用,把對(duì)應(yīng)的參數(shù)傳遞給了一個(gè)可以自定義的回調(diào)函數(shù)。這樣,這個(gè)WeatherDataManager就實(shí)現(xiàn)好了。

現(xiàn)在,回想起來(lái),我們?cè)谶@兩節(jié)中,關(guān)于model的部分,已經(jīng)寫了不少的代碼了,它們真的能正常工作么?我們?nèi)绾未_定這個(gè)事情呢?在把model關(guān)聯(lián)到controller之前,我們最好確定一下。

當(dāng)然,一個(gè)直觀的辦法就是在類似某個(gè)viewDidLoad之類的方法里,寫個(gè)代碼實(shí)際請(qǐng)求一下看看。但是估計(jì)你也能感覺(jué)到這種做法并不地道,如果未來(lái)你修改了Manager的代碼呢?難道還要重新找個(gè)viewDidLoad方法插個(gè)空來(lái)測(cè)試么?估計(jì)你自己都不太敢這樣做,萬(wàn)一你在恢復(fù)的時(shí)候不慎修改掉了哪部分功能代碼,就很容易隨隨便便坑上你幾個(gè)小時(shí)。

為此,我們需要一種更專業(yè)和安全的方式,來(lái)確定局部代碼的正確性。這種方式,就是單元測(cè)試。在開(kāi)始測(cè)試我們的WeatherDataManager之前,我們要先了解一下Xcode提供的單元測(cè)試模板。

了解單元測(cè)試模板

首先,在Xcode默認(rèn)創(chuàng)建的SkyTests分組中,刪掉默認(rèn)的SkyTests.swift。然后在SkyTests Group上點(diǎn)右鍵,選擇New File...

DarkSkyAndModel

其次,在右上角的filter中,輸入unit,找到單元測(cè)試的模板。選中Unit Test Case Class,點(diǎn)擊Next

DarkSkyAndModel

第三,給測(cè)試用例起個(gè)名字,例如WeatherDataManagerTest。這個(gè)名字最好可以直接表達(dá)我們要測(cè)試的內(nèi)容。這樣,不同的開(kāi)發(fā)者都可以方便的了解到實(shí)際測(cè)試的內(nèi)容:

DarkSkyAndModel

第四,接下來(lái),Xcode就會(huì)提示我們是否需要?jiǎng)?chuàng)建一個(gè)bridge header,由于我們?cè)诩僑wift環(huán)境中開(kāi)發(fā),因此,選擇Don't Create,并點(diǎn)擊Finish按鈕。

設(shè)置好保存路徑之后,我們就可以在SkyTests分組中,找到新添加的測(cè)試用例了。在開(kāi)始編寫測(cè)試之前,這個(gè)文件中有幾個(gè)值得說(shuō)明的地方:

首先,在文件一開(kāi)始,要添加下面的代碼引入項(xiàng)目的main module。這樣,才能在測(cè)試用例中,訪問(wèn)到項(xiàng)目定義的類型:

import XCTest
@testable import Sky

其次,在生成的代碼中,WeatherDataManagerTest派生自XCTestCase,表示這是一個(gè)測(cè)試用例。

第三,在WeatherDataManagerTest里,我們可以把所有的測(cè)試前要準(zhǔn)備的代碼,寫在setUp方法里,而把測(cè)試后需要清理的代碼,寫在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為我們生成了兩個(gè)默認(rèn)的測(cè)試方法:

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

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

要注意的是,所有測(cè)試方法都必須用test開(kāi)頭,Xcode才會(huì)識(shí)別它們并自動(dòng)執(zhí)行。這里,可以先把它們刪掉,稍后我們會(huì)編寫自己的測(cè)試方法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容