用Swift实现一款天气预报APP(二)
这个系列的目录:
上篇中主要讲了界面的一些内容,这篇主要讨论网络请求,获得天气的数据。具体的说是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之后已经可以看到天气预报的主界面了。