這個系列的目錄:
上篇中主要講了界面的一些內容,這篇主要討論網絡請求,獲得天氣的數據。具體的說是HTTP請求天氣站點的API,得到返回的JSON數據。解析這些數據,並更新到界面內容中。 讓用戶知道當前的和之后幾個小時的天氣狀況。
發起HTTP請求主要用到的是SDK的NSURLSession這個類,使用這個類對象可以創建請求任務並在這個任務中處理請求之后由服務器返回的JSON數據。在NSURLSession之前主要用到的是NSURLConnection。這兩個類比較類似。只是在NSURLSession中增加了后台執行的請求。發起網絡請求的時候,使用NSURLSession創建對應的NSURLSessionTask,並由這個Task請求服務器和處理返回的數據。
下面大體的看看我們怎么做HTTP請求的。本文將主要敘述如何發起HTTP請求。先講講使用最基本的iOS的SDK發請求,然后敘述如何用現在比較流向的AFNetworking框架請求。或許你也聽說過一個叫做ASIHttpRequest的框架,但是這個已經很久沒有人維護了。所以,這里就不再提及。
使用iOS SDK發起HTTP網絡請求:
1. 准備訪問服務器的NSURL對象。這個對象需要一個url字符串,比如百度的地址字符串就是“http://www.baidu.com”,我們這里需要一個指向天氣服務器的字符串。
var weatherUrl = "http://api.openweathermap.org/data/2.5/forecast?lat=\(latitude)&lon=\(longitude)" var url = NSURL(string: weatherUrl)
第一句的問好后面的部分?lat=\(latitude)&lon=\(longitude)是為url指定用戶當前的經緯度。之后根據這個url字符串生成NSURL對象。
2. 創建NSURLSession對象。NSURLSession有一個類方法創建實例。
self.urlSession = NSURLSession.sharedSession()
一般用到shareXXX的方式命名的方法是一個單例方法。也就是這個方法在被調用的時候會判斷需要的實例是否已經創建,如果是的話返回創建好的實例,如果沒有創建則初始化一個並保存起來以備下次使用。關於使用Swift實現單例模式,請參考這里。
3. 創建NSURLSessionDataTask,並設置好如何處理請求返回的數據。然后開始HTTP請求。
var task = self.urlSession.dataTaskWithURL(url!, completionHandler: {(data:NSData!, response: NSURLResponse!, error: NSError!) in if error != nil { println("http request error \(error)") return } println("\(response)") var httpResponse = response as NSHTTPURLResponse var statusCode: NSInteger = httpResponse.statusCode println("status code: \(statusCode)") var error: NSError? var jsonDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: &error) as NSDictionary if error != nil { println("json error") return } println("json \(jsonDictionary)") self.jsonLabel.text = jsonDictionary.description }) task.resume()
self.urlSession.dataTaskWithURL這個方法創建了一個DataTask。completionHandler后面的就是指定的處理返回數據的方法。這里使用了Swit的閉包。閉包的語法可以簡單的概括為{(參數列表。。。)->閉包的返回類型 in 功能代碼在這里}具體的參考上面的代碼示例。那么具體的,我們應該如何處理返回的數據呢。第一步,先查看返回的錯誤error是否為空。如果為空就是沒有錯,否則,就是有錯了。這個時候就可以提示用戶后直接return,不再處理后面的代碼了。
下面就是檢查response的statusCode。狀態碼最直觀的就是大家都見過的404,蝦米都木有找到的時候的提示。如果是200,那么就是請求服務器成功。否則,也可以提示用戶后返回了。
最后就是解析用戶數據了。首先需要把服務器返回的JSON格式的數據轉換為Swift可以直接訪問的NSDictionary。記住,這里是NSDictionary不是Swift基礎數據類型中的泛型Dictionary<KeyType, ValueType>。服務器的JSON數據轉換成NSDictionary后就可以取出需要的數據並更新到主界面上了。
這里你會發現很多的代碼調用都是通過NSError的實例是否為空判斷某函數的執行是否有錯誤發生的。Swift沒有try-catch的異常處理模式。只有這樣的error的方式。這個大家需要習慣。用這種方式處理錯誤是為了去掉代碼的二意性。有其他語言編程經理的都知道,有時候就用try-catch來做代碼的某些判斷了。這是不對的。
最后調用task的resume方法開始HTTP請求。
前文已經簡單的提到過定位的功能。本文在這里之前都在討論HTTP請求的功能。如前面提到的,請求天氣數據到時候需要用到經緯度的數據作為url參數。所以HTTP請求只能在定位成功獲取到用戶當前的經緯度之后進行。所以,在代碼實現的時候,網絡請求在Location Manager的定位成功的代理方法中發起。
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!){ println("get location") var location:CLLocation = locations[locations.count-1] as CLLocation if (location.horizontalAccuracy > 0) { self.locationManager.stopUpdatingLocation() println(location.coordinate) self.textLabel.text = "latitude \(location.coordinate.latitude) longitude \(location.coordinate.longitude)" // 在這里發起HTTP請求 self.updateWeatherWith(location.coordinate.latitude, longitude: location.coordinate.longitude) } }
到此為止,從獲取用戶位置到使用用戶的經緯度數據請求天氣服務器獲取天氣的JSON數據的功能都已經銜接在一起了。
那么,我們來討論一下如何使用AFNetworking這個框架(framework)。在這之前,用戶需要配置cocoaPods。具體的步驟可以參考這里。這里必須吐槽一下,Ruby什么的編程之類的網站多要牆真是不可理喻啊。配置好之后,親,你一定要點擊的時候workspace那個后綴的文件,不是項目文件了。否則會出錯。
要使用AFNetworking框架就涉及到一個Objective-C和Swift交互的問題了。
let manager = AFHTTPRequestOperationManager()
這行代碼直接編譯不通過。。。稍微深究機會發現,在Swift中沒有辦法直接使用OC(Objective-C)的代碼。翻翻項目,找到SwiftWeather-Bridging-Header.h頭文件,然后在里面添加對於AFNetworking框架的引用。
#import <AFNetworking/AFNetworking.h>
添加后,編譯你的項目。上面那行出錯的代碼就可以用了。
使用AFNetworking框架確實會很方便。不用像使用NSURLSession里那樣寫那么多的代碼。這個通過一個簡單的感官比較就會得出結論。先在上AFNetworking的HTTP請求代碼。
let manager = AFHTTPRequestOperationManager() let url = "http://api.openweathermap.org/data/2.5/forecast" println(url) let params = ["lat":latitude, "lon":longitude, "cnt":0] println(params) manager.GET(url, parameters: params, success: { (operation: AFHTTPRequestOperation!, responseObject: AnyObject!) in //println("JSON: " + responseObject.description!) self.updateUISuccess(responseObject as NSDictionary!) }, failure: { (operation: AFHTTPRequestOperation!, error: NSError!) in println("Error: " + error.localizedDescription) self.loading.text = "Internet appears down!" })
初始化一個AFHTTPRequestOperationManager來處理請求和數據返回等的處理,一個類就夠了。不用task什么的了。指定要訪問的url字符串,這里是字符串也不需要NSURL的實例了。然后把需要給url字符串添加的參數放在一個Dictionary<String, String>泛型字典中。然后用manager發出HTTP請求,並指定了請求的方式為GET,函數的名字就是HTTP請求的方式。HTTP請求還有除GET之外的很多中,其中最常用的是POST。然后可以看到GET方法中的sucess和failure,都分別是在指定請求成功的處理代碼和失敗的處理代碼。
請求數據不是總能成功。這在代碼中也有體現。但是不成功的數據請求並不只是請求不到數據,比如在網絡不通的時候。還包括請求到了數據,但是數據表明這個請求是錯誤的。所以,在網絡連接失敗而造成的網絡請求失敗時提醒用戶“Internet apears down”。在數據解析后發現服務器返回數據提示說數據錯誤,這個時候也要提醒用戶錯誤。這里只是點到,不做其他處理。讀者在實際的開發中需要注意這一點。
數據請求完成后,調用方法updateUISuccess把數據顯示在界面元素中。從上到下,依次是用戶所在地(文字),天氣(圖片),溫度(文字)。然后在下面,從左到右,依次顯示這一天中其他幾個小時 的天氣預報。
func updateUISuccess(jsonResult: NSDictionary) { self.loading.text = nil self.loadingIndicator.hidden = true self.loadingIndicator.stopAnimating() if let tempResult = ((jsonResult["list"]? as NSArray)[0]["main"] as NSDictionary)["temp"] as? Double { // If we can get the temperature from JSON correctly, we assume the rest of JSON is correct. var temperature: Double var cntry: String cntry = "" if let city = (jsonResult["city"]? as? NSDictionary) { if let country = (city["country"] as? String) { cntry = country if (country == "US") { // Convert temperature to Fahrenheit if user is within the US temperature = round(((tempResult - 273.15) * 1.8) + 32) } else { // Otherwise, convert temperature to Celsius temperature = round(tempResult - 273.15) } // FIXED: Is it a bug of Xcode 6? can not set the font size in IB. //self.temperature.font = UIFont.boldSystemFontOfSize(60) self.temperature.text = "\(temperature)°" } if let name = (city["name"] as? String) { self.location.font = UIFont.boldSystemFontOfSize(25) self.location.text = name } } if let weatherArray = (jsonResult["list"]? as? NSArray) { for index in 0...4 { if let perTime = (weatherArray[index] as? NSDictionary) { if let main = (perTime["main"]? as? NSDictionary) { var temp = (main["temp"] as Double) if (cntry == "US") { // Convert temperature to Fahrenheit if user is within the US temperature = round(((temp - 273.15) * 1.8) + 32) } else { // Otherwise, convert temperature to Celsius temperature = round(temp - 273.15) } //FIXED: Is it a bug of Xcode 6? can not set the font size in IB. //self.temperature.font = UIFont.boldSystemFontOfSize(60) if (index == 1) { self.temp1.text = "\(temperature)°" } if (index == 2) { self.temp2.text = "\(temperature)°" } if (index == 3) { self.temp3.text = "\(temperature)°" } if (index == 4) { self.temp4.text = "\(temperature)°" } } var dateFormatter = NSDateFormatter() dateFormatter.dateFormat = "HH:mm" if let date = (perTime["dt"]? as? Double) { let thisDate = NSDate(timeIntervalSince1970: date) let forecastTime = dateFormatter.stringFromDate(thisDate) if (index==1) { self.time1.text = forecastTime } if (index==2) { self.time2.text = forecastTime } if (index==3) { self.time3.text = forecastTime } if (index==4) { self.time4.text = forecastTime } } if let weather = (perTime["weather"]? as? NSArray) { var condition = (weather[0] as NSDictionary)["id"] as Int var icon = (weather[0] as NSDictionary)["icon"] as String var nightTime = false if icon.rangeOfString("n") != nil{ nightTime = true } self.updateWeatherIcon(condition, nightTime: nightTime, index: index) if (index == 4) { return } } } } } } self.loading.text = "Weather info is not available!" }
然后,根據不同的解析結果,跟新當前的和后面幾個小時的天氣調用方法updateWeatherIcon、updatePictures更新天氣圖片(白天、晚上、天氣)。示例工程中會有詳細的實現。這里略去不提。
這個時候,運行APP之后已經可以看到天氣預報的主界面了。