Swift 中处理 JSON 数据有很多种方式,可以使用原生的 NSJSONSerialization,也可以使用很多第三方库。原生的 NSJSONSerialization 方式这篇文章中介绍过。这次我们介绍一个第三方库 SwiftyJSON
并且用它来制作一个有趣的 APP.
关于 SwiftyJSON
首先,我们来了解一下什么是 SwiftyJSON
, 并且我们为什么要用这个库。比如我们要解析这个比特币实时价格的接口:
这个接口的数据格式如下:
{
"time": {
"updated": "Jul 20, 2015 13:14:00 UTC",
"updatedISO": "2015-07-20T13:14:00+00:00",
"updateduk": "Jul 20, 2015 at 14:14 BST"
},
"disclaimer": "This data was produced from the CoinDesk Bitcoin Price Index (USD & CNY respectively).",
"bpi": {
"USD": {
"code": "USD",
"rate": "278.3400",
"description": "United States Dollar",
"rate_float": 278.34
},
"CNY": {
"code": "CNY",
"rate": "1,717.4683",
"description": "Chinese Yuan",
"rate_float": 1717.4683
}
}
}
如果我们使用原生的 NSJSONSerialization
方式,得到比特币的人民币价格的话,我们写出的代码大概就是这样的:
var url = "http://api.coindesk.com/v1/bpi/currentprice/CNY.json"
if let jsonData = NSData(contentsOfURL: NSURL(string: url)!) {
if let jsonObj: NSDictionary = NSJSONSerialization.JSONObjectWithData(jsonData, options: .MutableLeaves, error: nil) as? NSDictionary {
if let bpi:NSDictionary = jsonObj["bpi"] as? NSDictionary {
if let cny:NSDictionary = bpi["CNY"] as? NSDictionary {
print(cny["rate"]!)
}
}
}
}
那么我们再来看一下,我们用 SwiftyJSON 来达到同样的目的要写的代码:
let url = "http://api.coindesk.com/v1/bpi/currentprice/CNY.json"
if let jsonData = NSData(contentsOfURL: NSURL(string: url)!) {
let json = JSON(data: jsonData)
print(json["bpi"]["CNY"]["rate"])
}
是不是感觉精简了很多呢,对,就是这个效果。SwiftyJSON
的以大好处就是,不用你来处理 Swift 中的类型转换,它会自动帮你处理类型等开发语言相关的问题,让你专注于 JSON 数据的处理中。怎么样,挺好用的把。
关于 SwifyJSON 的更多介绍,大家还可以参看它的 Github 主页:
https://github.com/SwiftyJSON/SwiftyJSON
下面我们就以一个例子来继续了解 SwiftyJSON。
比特币查询应用
我们今天要做的是一个比特币实时价格的 APP,这里我们会用到 SwiftyJSON 来解析服务端的数据。
首先我们创建一个项目, Single View Application
类型:
然后设置好项目的基本信息:
然后就是要引入 SwiftyJSON
库,
另外还可以下载我们预配置好的项目来进行开发:bitprice-start.zip
现在我们就进入主题吧,首先我们开始构建 UI 界面,打开 Main.storyboard
进行编辑。
- 首先,我们在
storyboard
中拖入三个UILabel
![]((http://www.swiftcafe.io/images/swifty-json/3.png)
其中第一个 Label 的 text
属性设置为 "当前价格", 后两个 Label 的 text
设置为空,用作显示比特币的价格。
- 然后,我们将两个用于显示价格的
UILabel
链接到主控制器的Outlet
中,在打开 storyboard 视图的同时,按住Option
并点击ViewController.swift
。这样编辑界面上同时显示了storyboard
和控制器的代码,然后我们在storyboard
中选中 Label,然后按住control
拖动到控制器的代码中:
![]((http://www.swiftcafe.io/images/swifty-json/4.jpg)
随后会弹出一个变量名称提示框,我们将第一个 UILabel 命名为 priceLabel
,将第二个 UILabel 命名为 differLabel
。
![]((http://www.swiftcafe.io/images/swifty-json/5.jpg)
最后,我们在给 ViewController
建立一个新的属性 lastPrice
, 存储上次更新的价格,用于计算当前价格相对于上次的涨跌幅。
这样我们的 ViewController
的属性定义如下:
class ViewController: UIViewController {
@IBOutlet var priceLabel: UILabel!
@IBOutlet var differLabel: UILabel!
var lastPrice:Double = 0.0
}
两个 IBOutlet
链接的 UILabel
, 还有一个 Double
变量用于存放上次的价格。
基础结构设置好后,我们就可以开始构建应用的逻辑了,我们首先定义一个方法 getLatestPrice()
,用于获取比特币最新的价格:
func getLatestPrice() -> String?{
let url = "http://api.coindesk.com/v1/bpi/currentprice/CNY.json"
if let jsonData = NSData(contentsOfURL: NSURL(string: url)!) {
let json = JSON(data: jsonData)
return json["bpi"]["CNY"]["rate"].stringValue
}else {
return nil
}
}
这里面我们首先通过 NSData
的构造方法从指定的 URL 地址读取了比特币价格数据,然后用到了 SwiftyJSON
来读取和解析返回的 JSON
数据
let json = JSON(data: jsonData)
return json["bpi"]["CNY"]["rate"].stringValue
只有两行代码,就完成了数据的提取,很方便吧。
数据读取方法写好了,那么我们需要另外一个方法来调度这个,因为我们这个 getLatestPrice
的网络操作时同步的,所以我们的调度方法需要把它放到另外的线程中,我们使用 GCD
进行这个处理:
func reloadPrice() {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
let price = self.getLatestPrice()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector("reloadPrice"), userInfo: nil, repeats: false)
if let p = price {
var nsPrice = p as NSString
nsPrice = nsPrice.stringByReplacingOccurrencesOfString(",", withString: "")
let doublePrice = nsPrice.doubleValue
let differPrice = doublePrice - self.lastPrice
self.lastPrice = doublePrice;
self.priceLabel.text = NSString(format: "¥ %.2f", doublePrice) as? String
if differPrice > 0 {
self.differLabel.textColor = UIColor.redColor()
self.priceLabel.textColor = UIColor.redColor()
self.differLabel.text = NSString(format: "+%.2f", differPrice) as? String
}else{
self.differLabel.text = NSString(format: "%.2f", differPrice) as? String
self.differLabel.textColor = UIColor.greenColor()
self.priceLabel.textColor = UIColor.greenColor()
}
}
})
});
}
我们这里首先使用 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),...)
来调度异步线程,在这个线程中,我们调用了 getLatestPrice()
方法来获取当前的比特币价格,读取成功后,我们要用这个数据来更新 UI 显示了。而 UI 的操作时不能在异步线程中进行的。所以我们随后又调用了 dispatch_async(dispatch_get_main_queue(),...)
方法将处理调度到主线程中。
由于服务端返回的数据格式是字符串类型的诸如这样的价格数据
1,273.203
所以我们还需要对这个数据进行一下转换:
var nsPrice = p as NSString
nsPrice = nsPrice.stringByReplacingOccurrencesOfString(",", withString: "")
let doublePrice = nsPrice.doubleValue
首先我们将字符串中的 ,
字符清除掉,然后使用 NSString 的 doubleValue
将字符串转换成 Double 类型。
接下来,我们用当前的价格减去上次读取的价格,计算出差价,就可以显示出相对于上次读取数据的涨跌幅度了。计算完成后,我们就重新将当前的价格存入 self.lastPrice
中,以便于下次的计算。
let differPrice = doublePrice - self.lastPrice
self.lastPrice = doublePrice;
最后,我们计算出了这些数据,再将他们显示的 UILabel 上面。
self.priceLabel.text = NSString(format: "¥ %.2f", doublePrice) as? String
if differPrice > 0 {
self.differLabel.textColor = UIColor.redColor()
self.priceLabel.textColor = UIColor.redColor()
self.differLabel.text = NSString(format: "+%.2f", differPrice) as? String
}else{
self.differLabel.text = NSString(format: "%.2f", differPrice) as? String
self.differLabel.textColor = UIColor.greenColor()
self.priceLabel.textColor = UIColor.greenColor()
}
我们首先将当前价格设置到 self.priceLabel
, 然后根据涨跌幅度是正数还是负数设置 self.differLabel
的文字,如果是正数要在前面放一个 +
号。同时我们根据涨跌幅设置文本的颜色,如果是涨就设置为红色,如果是跌就设置为绿色。
最后还有一行代码我们要注意:
NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector("reloadPrice"), userInfo: nil, repeats: false)
我们用 NSTimer
又调度了一下这个方法,在 3 秒钟之后,重新请求最新价格。这样我们的价格就能每隔 3 秒刷新一次。
数据读取方法弄好之后,我们就可以在 viewDidLoad()
里面调用它了
override func viewDidLoad() {
super.viewDidLoad()
reloadPrice()
}
接下来可以运行一下项目,我们就会看到报价比特币的最新价格显示在界面上了。然后还可以不停的刷新。
显示历史报价
最新报价的现实逻辑我们实现完了,我们还可以做更多的事情,仔细研究 coindesk
的数据,我们发现还有一个接口可以实现查询比特币的报价历史:
http://api.coindesk.com/v1/bpi/historical/close.json?start=2015-07-15&end=2015-07-24¤cy=CNY
访问这个接口我们就可以看到诸如这样的数据返回:
{
"bpi": {
"2015-07-15": 1756.5732,
"2015-07-16": 1719.6188,
"2015-07-17": 1723.7974,
"2015-07-18": 1698.9991,
"2015-07-19": 1686.3934,
"2015-07-20": 1723.3102,
"2015-07-21": 1702.5693,
"2015-07-22": 1710.3503
},
"disclaimer": "This data was produced from the CoinDesk Bitcoin Price Index. BPI value data returned as CNY.",
"time": {
"updated": "Jul 23, 2015 09:53:17 UTC",
"updatedISO": "2015-07-23T09:53:17+00:00"
}
}
我们看到,这个接口返回了从起始日期到结束日期的比特币价格信息,我们可以使用这个数据来显示历史数据,比如从当天往前 5 天之内的历史数据。
那么我们先写一个网络读取和解析数据的方法:
func getLastFiveDayPrice() -> Array<(String,String)> {
var curDate = NSDate()
var calendar = NSCalendar.currentCalendar()
let startDate = calendar.dateByAddingUnit(NSCalendarUnit.CalendarUnitDay, value: -6, toDate: curDate, options: nil)
let endDate = calendar.dateByAddingUnit(NSCalendarUnit.CalendarUnitDay, value: -1, toDate: curDate, options: nil)
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let url = "http://api.coindesk.com/v1/bpi/historical/close.json?start=\\\\(formatter.stringFromDate(startDate!))&end=\\\\(formatter.stringFromDate(endDate!))¤cy=CNY"
var result = Array<(String,String)>()
if let jsonData = NSData(contentsOfURL: NSURL(string: url)!) {
let json = JSON(data: jsonData)
let bpiDict:JSON = json["bpi"]
for (key,val) in bpiDict {
result.append((key,val.stringValue))
}
}
return result
}
这个方法会返回一个数组,我们仔细看一下这个数组的定义 Array<(String,String)>
,数组中的类型是 (String,String)
, 这种类型定义叫做 元组(Tuple) 是 Swift中的一个语言特性,关于元组,简而言之就是一个包含了多个元素的类型,比如我们这里的元组包含了两个 String
类型的值。
下面展示了元组类型的简单用法:
let tuple = ("2012-2-21","1,232.23")
//可以通过索引来引用元组的元素
print("\\\\(tuple.0) price is \\\\(tuple.1)")
//还可以为元组的项制定名称
let (date,price) = tuple
print("\\\\(date) price is \\\\(price)")
我们看到,我们可以通过索引的方式,也可以通过为元组项指定名称的方式来引用元组中的值。这里简单介绍一下元组的概念,更详细的内容大家可以参考相关资料。
接下来,我们看一下这个方法的内容,首先我们通过格式化 NSDate
输出的方式拼接出 URL,这里我们用到了 NSCalendar
,这个类可以通过 dateByAddingUnit
方法操作 NSDate
的各个日期属性,比如将当前的日期减去多少天,我们用这个方法得到当前日期往前 5 天和 1 天的日期值,用于得到这个期间的比特币价格。
我们还用到了 NSDateFormatter
,这个类可以将 NSDate
的值进行格式化输出,得到我们需要的日期输出格式。我们这里需要类似 2012-03-12
的这种日期格式,所以我们将日期格式定义为 yyyy-MM-dd
。
最后通过 NSDateFormatter
的 stringFromDate
方法输出格式化后的日期值:
var curDate = NSDate()
var calendar = NSCalendar.currentCalendar()
let startDate = calendar.dateByAddingUnit(NSCalendarUnit.CalendarUnitDay, value: -6, toDate: curDate, options: nil)
let endDate = calendar.dateByAddingUnit(NSCalendarUnit.CalendarUnitDay, value: -1, toDate: curDate, options: nil)
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let url = "http://api.coindesk.com/v1/bpi/historical/close.json?start=\\\\(formatter.stringFromDate(startDate!))&end=\\\\(formatter.stringFromDate(endDate!))¤cy=CNY"
拼接好 URL 之后,我们就可以开始请求数据了,看一看下面的代码:
var result = Array<(String,String)>()
if let jsonData = NSData(contentsOfURL: NSURL(string: url)!) {
let json = JSON(data: jsonData)
let bpiDict:JSON = json["bpi"]
for (key,val) in bpiDict {
result.append((key,val.stringValue))
}
}
首先我们定义了一个 result
数组,用于返回我们的价格列表。然后我们使用 NSData
的构造方法来请求接口的数据。请求到数据后,我们使用 SwiftyJSON
的 JSON
类进行解析,随后的 for
循环中,我们遍历了 bpi
节点中的所有的键值,将这些键值通过元组的方式添加到 result
列表中。
result.append((key,val.stringValue))
注意条语句,我们构造元组的方式 (key,val.stringValue)
, 因为我们的元组定义为 (String,String)
类型,在 for
循环中,我们的 key
变量是 String
类型的,所以我们可以直接用这个值来构建元组的第一项,而 val
不是 String
类型的。我们必须使用 SwiftyJSON
中的 stringValue
方法取得这个节点的 String
类型的值来构建元组的第二项。
到此为止我们的历史数据读取方法也完成了。
构造历史价格界面
数据读取方法构造完成后,我们就可以开始处理 UI 界面了,我们创建了 buildHistoryLabels
方法:
func buildHistoryLabels(priceList: Array<(String,String)>) {
var count = 0.0
var labelTitle = UILabel(frame: CGRectMake(CGFloat(30.0), CGFloat(220.0), CGFloat(200.0), CGFloat(30.0)))
labelTitle.text = "历史价格"
self.view.addSubview(labelTitle)
for (date, price) in priceList {
var labelHistory = UILabel(frame: CGRectMake(CGFloat(30.0), CGFloat(250 + count * 40.0), CGFloat(200.0), CGFloat(30.0)))
labelHistory.text = "\\\\(date) \\\\(price)"
self.view.addSubview(labelHistory)
count++
}
}
这个方法接受一个数组作为参数,这个数组的内容就是我们的价格列表。首先我们这里构建了这组 UILabel 的标题:
var labelTitle = UILabel(frame: CGRectMake(CGFloat(30.0), CGFloat(220.0), CGFloat(200.0), CGFloat(30.0)))
labelTitle.text = "历史价格"
self.view.addSubview(labelTitle)
然后我们通过一个 for
循环来遍历价格列表,取出元组的两项内容,分别以 date
和 price
来命名,并用这些数据构建出 UILabel
并添加到 UI 视图中:
for (date, price) in priceList {
var labelHistory = UILabel(frame: CGRectMake(CGFloat(30.0), CGFloat(250 + count * 40.0), CGFloat(200.0), CGFloat(30.0)))
labelHistory.text = "\\\\(date) \\\\(price)"
self.view.addSubview(labelHistory)
count++
}
现在我们可以运行 APP 了,我们看到当前的价格,以及近期的价格都展示在了界面中:
到此为止,我们利用 SwiftyJSON
完成的读取了 JSON 数据。我们的比特币查询 APP 也基本完成了。当然这个示例 APP 还有很多不完善的地方,如果大家有兴趣,让他变的更加完善。