本文是Intermediate iOS 11 Programming with Swift 4系列 的 第 四 篇.
JSON是什么?
JSON (JavaScript对象表示法的简称)是一种基于文本的、轻量级的、易于存储和交换数据的方法。它通常用于表示客户机-服务器应用程序中的结构化数据和数据交换,作为XML的替代品。我们每天使用的许多web服务都有基于json的api。包括Twitter、Facebook和Flickr在内的大多数iOS应用程序都以JSON格式将数据发送到后端web服务。
例如:
如您所见,JSON格式的数据比XML更易于阅读和解析。我不会详细介绍JSON。这不是本章的目的。如果您想了解更多关于该技术的信息,我建议您访问json官网的JSON指南。
自从ios5发布以来,iOS SDK已经让开发者可以轻松地获取和解析JSON数据。它附带了一个名为NSJSONSerialization的方便类,可以自动将JSON格式的数据转换为对象。在本章的后面,我将向您展示如何使用API来解析web服务返回的一些JSON格式的示例数据。一旦您了解了它的工作原理,就可以通过与其他免费/付费web服务集成来构建一个应用程序。
在Swift 4(或iOS11)中,苹果引入了可编码协议来简化整个JSON归档和序列化过程。我们还将研究这个新特性,看看如何在JSON解析中应用它。
Demo App
“像往常一样,我们会创建一个演示应用程序。我们叫它KivaLoan吧。”我们之所以命名KivaLoan应用,是因为我们将利用kiva.org提供的基于jsf的API。
如果你还没听说过Kiva,它是一个非营利组织,它的使命是“通过贷款帮助人们减轻贫困”。它允许个人贷出至多25美元,以帮助在世界各地创造机会。Kiva为开发者提供免费的基于web的api来访问他们的数据。对于我们的演示应用程序,我们将调用以下Kiva API来检索最近的筹款贷款,并将它们显示在表视图中:
https://api.kivaws.org/v1/loans/newest.json
小提示:从ios9开始,苹果推出了一项名为应用程序传输安全(ATS)的功能,目的是提高应用程序和web服务之间连接的安全性。默认情况下,所有出站连接都应该使用HTTPS。否则,您的应用程序将不允许连接到web服务。您可以选择在Info中添加一个名为nsallowsarbitraryload的键。plist并将值设置为YES以禁用ATS,以便您可以通过HTTP连接到web api. 然而,如果你在你的应用程序中使用nsallowsarbitraryload的话,你必须注意这一点。在ios10系统中,苹果进一步加强了所有iOS应用的ATS系统。到2017年1月,所有iOS应用程序都应该兼容ats。换句话说,如果您的应用程序连接到任何外部web服务,那么连接必须通过HTTPS。如果你的应用不能满足这个要求,苹果将不会允许它在应用商店发布。
上述API的返回数据为JSON格式。下面是一个示例结果:
为了让您专注于学习JSON实现,您可以首先从这里下载项目模板。我已经为你创建了这个应用的框架。它是一个简单的基于表格的应用程序,它显示了Kiva.org提供的贷款列表。项目模板包括为表视图控制器和原型单元格预先构建的故事板和自定义类。如果你运行模板,它会产生一个空的表格应用。
创建JSON数据模型
我们将首先创建一个类来建模贷款。加载JSON不是必需的,但是最佳实践是创建一个单独的类(或结构)来存储数据模型。贷款类表示KivaLoan应用程序中的贷款信息,用于存储Kiva.org返回的贷款信息。为了简单起见,我们不会使用所有返回的贷款数据。相反,应用程序将只显示以下贷款字段:
贷款申请人的名字:
name = " Mar\U00e8me "
贷款申请人的国家:
贷款做什么:
use = " to buy fabric resell " ;
贷款金额:
" loan_amount" = 750;
这些字段足以填充表格视图中的标签。”现在使用Swift文件模板创建一个新的类文件。命名为贷款。swift并声明贷款结构如下:
struct Loan {
var name: String = ""
var country: String = ""
var use: String = ""
var amount: Int = 0
}
JSON支持一些基本数据类型,包括数字、字符串、布尔值、数组和对象(键和值对的关联数组)。对于贷款字段,贷款金额作为数字值存储在json格式的数据中。这就是为什么我们用Int类型声明amount属性。对于其他字段,它们是用String类型声明的。
用Kiva API取回贷款
Kiva API是免费使用的。不需要注册。点击这里获得JSON格式的最新融资贷款。
KivaLoanTableViewController.swift并在开始时声明两个变量:
private let kivaLoanURL = "https://api.kivaws.org/v1/loans/newest.json"
private var loans = [Loan] ()
我们刚刚定义了Kiva API的URL,并声明用于存储一系列贷款对象的贷款变量。接下来,在同一个文件中插入以下方法:
func getLatestLoans() {
guard let loanUrl = URL(string: kivaLoanURL) else {
return
}
let request = URLRequest(url: loanUrl)
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
if let error = error {
print(error)
return
}
// Parse JSON data
if let data = data {
self.loans = self.parseJsonData(data: data)
// Reload table view
OperationQueue.main.addOperation({
self.tableView.reloadData()
})
}
})
task.resume()
}
func parseJsonData(data: Data) -> [Loan] {
var loans = [Loan]()
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary
// Parse JSON data
let jsonLoans = jsonResult?["loans"] as! [AnyObject]
for jsonLoan in jsonLoans {
let loan = Loan()
loan.name = jsonLoan["name"] as! String
loan.amount = jsonLoan["loan_amount"] as! Int
loan.use = jsonLoan["use"] as! String
let location = jsonLoan["location"] as! [String:AnyObject]
loan.country = location["country"] as! String
loans.append(loan)
}
} catch {
print(error)
}
return loans
}
这两种方法构成了应用程序的核心部分。这两种方法协作调用Kiva API,检索JSON格式的最新贷款,并将JSON格式的数据转换为贷款对象数组。让我们详细地讨论一下。在getLatestLoans方法中,我们首先用Kiva贷款API的URL实例化URL结构。初始化返回一个可选的。这就是为什么我们使用guard关键字来查看可选项是否具有值。如果不是,我们只是返回并跳过方法中的所有代码。
接下来,我们用load URL创建一个URLSession。URLSession类为通过HTTP和HTTPS处理在线内容提供了api。共享会话对于发出简单的HTTP/HTTPS请求来说已经足够了。如果您必须支持自己的网络协议,那么URLSession还为您提供了创建自定义会话的选项。
URLSession的一个优点是,您可以添加一系列会话任务来处理数据加载,以及从服务器上传和下载文件和数据获取(例如JSON数据获取)。
通过会话,您可以安排三种类型的任务:数据任务(URLSessionDataTask)用于检索数据到内存,下载任务(URLSessionDownloadTask)用于下载文件到磁盘,以及上传任务(URLSessionUploadTask)用于从磁盘上传文件。在这里,我们使用data任务从Kiva.org检索内容。要向会话添加数据任务,我们使用特定的URL请求调用dataTask方法。添加任务后,会话将不会采取任何操作。您必须调用resume方法(即task.resume())来启动数据任务。
与大多数网络API一样,URLSession API也是异步的。一旦请求完成,它通过调用完成处理程序返回数据(以及错误)。
在完成处理程序中,在返回数据之后,我们检查是否有错误。如果没有找到错误,我们调用parseJsonData方法。
返回的数据是JSON格式。我们创建了一个名为parseJsonData的助手方法,用于将给定的json格式的数据转换为一个贷款对象数组。Foundation框架提供了JSONSerialization类,它能够将JSON转换为Foundation对象,并将Foundation对象转换为JSON。在代码片段中,我们使用给定的JSON数据调用jsonObject方法来执行转换。
当将JSON格式的数据转换为对象时,通常将顶级项转换为字典或数组。在本例中,Kiva API返回数据的顶层被转换为字典。您可以使用关键贷款访问贷款数组。
你怎么知道用什么Key?
您可以使用一个JSON浏览器来引用API文档或测试JSON数据(例如http://jsonviewer.stack.hu)。如果您已经将Kiva API加载到JSON浏览器中,这里是结果的一个例外
{
"paging": {
"page": 1,
"total": 5297,
"page_size": 20,
"pages": 265
},
"loans": [
{
"id": 794429,
"name": "Joel",
"description": {
"languages": [
"es",
"en"
]
},
"status": "fundraising",
"funded_amount": 0,
"basket_amount": 0,
"image": {
"id": 1729143,
"template_id": 1
},
"activity": "Home Appliances",
"sector": "Personal Use",
"use": "To buy home appliances.",
"location": {
"country_code": "PE",
"country": "Peru",
"town": "Ica",
"geo": {
"level": "country",
"pairs": "-10 -76",
"type": "point"
}
},
"partner_id": 139,
"posted_date": "2015-11-20T08:50:02Z",
"planned_expiration_date": "2016-01-04T08:50:02Z",
"loan_amount": 400,
"borrower_count": 1,
"lender_count": 0,
"bonus_credit_eligibility": true,
"tags": [
]
},
{
"id": 797222,
"name": "Lucy",
"description": {
"languages": [
"en"
]
},
"status": "fundraising",
"funded_amount": 0,
"basket_amount": 0,
"image": {
"id": 1732818,
"template_id": 1
},
"activity": "Farm Supplies",
"sector": "Agriculture",
"use": "To purchase a biogas system for clean cooking",
"location": {
"country_code": "KE",
"country": "Kenya",
"town": "Gatitu",
"geo": {
"level": "country",
"pairs": "1 38",
"type": "point"
}
},
"partner_id": 436,
"posted_date": "2016-11-20T08:50:02Z",
"planned_expiration_date": "2016-01-04T08:50:02Z",
"loan_amount": 800,
"borrower_count": 1,
"lender_count": 0,
"bonus_credit_eligibility": false,
"tags": [
]
},
...
正如您从上面的代码中看到的,分页和贷款是两个顶级项目。一旦JSONSerialization类转换JSON数据,结果(即jsonResult)将作为字典返回,顶级项作为键。这就是为什么我们可以使用关键贷款来获得贷款。这一行代码供您参考:
let jsonLoans = jsonResult?["loans"] as! [AnyObject]
返回贷款数组(即jsonLoans)后,我们对数组进行循环。每个数组项(即jsonLoan)都被转换为字典。在循环中,我们从每个字典中提取贷款数据,并将它们保存到一个loan对象中。同样,通过研究JSON结果,您可以找到键(以黄色突出显示)。特定结果的值存储为AnyObject。之所以使用AnyObject,是因为JSON值可以是字符串、Double、Boolean、数组、字典或null。这就是为什么要将值向下转换为特定类型的原因,例如String和Int。最后,我们将贷款对象放入贷款数组中,这是方法的返回值。
for jsonLoan in jsonLoans {
var loan = Loan()
loan.name = jsonLoan["name"] as! String
loan.amount = jsonLoan["loan_amount"] as! Int
loan.use = jsonLoan["use"] as! String
let location = jsonLoan["location"] as! [String: AnyObject]
loan.country = location["country"] as! String
loans.append(loan)
}
在解析JSON数据并返回贷款数组之后,我们调用reloadData方法来重新加载表。您可能想知道为什么我们需要调用OperationQueue.main。在主线程中添加和执行数据重载。数据任务的完成处理程序中的代码块在后台线程中执行。如果在后台线程中调用reloadData方法,则不会立即重新加载数据。为了确保响应的GUI更新,应该在主线程中执行此操作。这就是为什么我们调用OperationQueue.main。addOperation方法和请求在主队列中运行reloadData方法。
OperationQueue.main.addOperation({
self.tableView.reloadData()
})
注意:您还可以使用dispatch_async函数在主线程中执行一个代码块。但是根据Apple的说法,建议在dispatch_async上使用OperationQueue。一般来说,苹果建议使用最高级的api,而不是降级为低级的api。
在TableView中显示贷款
有了贷款数组,我们最不需要做的就是在表视图中显示数据。”在kivaloantableviewcontroller中更新以下方法:
override func numberOfSections(in tableView: UITableView) -> Int {
// Return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows
return loans.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! KivaLoanTableViewCell
// Configure the cell...
cell.nameLabel.text = loans[indexPath.row].name
cell.countryLabel.text = loans[indexPath.row].country
cell.useLabel.text = loans[indexPath.row].use
cell.amountLabel.text = "$\(loans[indexPath.row].amount)"
return cell
}
如果您熟悉UITableView的实现,那么上面的代码非常简单。在tableView(_:cellForRowAt:)方法中,我们从贷款数组中检索贷款信息,并在自定义表单元格中填充它们。需要注意的一件事是下面的代码:
"$\(loans[indexPath.row].amount)"
在某些情况下,您可能希望创建一个字符串(例如$)和integer(例如loan [indexPath.row].amount])。Swift提供了一种强大的方法来创建这些类型的字符串,称为字符串插值。您可以使用上面的语法。
最后,在viewDidLoad方法中插入以下代码行来开始获取贷款数据: getLatestLoans()
运行App
在 这里下载Xcode 项目
Codable 介绍
Swift 4引入了一种使用可编码方式对JSON数据进行编码和解码的新方法。我们将使用这种新方法重写演示应用程序的JSON解码部分。在我们开始修改之前,让我给你一个关于可编程的基本步骤。如果您查看可编程的文档,它只是协议组合的一种类型别名:
typealias Codable = Decodable & Encodable
Decodable 和 Encodable 是您需要处理的两个实际协议。但是,为了方便起见,我们通常使用这个类型别名来处理JSON编码和解码。
首先,与传统的编码/解码JSON方法相比,使用 codable 的优势是什么?如果你回到前一节再读一遍代码,你会发现我们必须手动解析JSON数据,将其转换为字典并创建贷款对象。
Codable为开发者提供了一种不同的解码(或编码)JSON的方式,从而简化了整个过程。只要您的类型符合可编码协议,以及新的JSONDecoder,您就能够将JSON数据解码到指定的实例中。图4.3演示了如何使用JSONDecoder将示例贷款数据解码为一个贷款实例。
JSON 解码
为了让您更好地了解可编程的工作方式,让我们启动一个playground项目并编写一些代码。创建了playground项目后,声明以下json变量:
let json = """
{
"name": "John Davis",
"country": "Peru",
"use": "to buy a new collection of clothes to stock her shop before the holidays.",
"amount": 150
}
"""
我们首先从基础开始。这里我们定义一个非常简单的JSON数据,包含4个条目。前三个项目的值的类型字符串最后一个Int类型。边注,如果这是你第一次看到一双三重引号("""), Swift4中介绍了这个语法来声明与多行字符串。
接下来,像这样写贷款结构:
struct Loan: Codable {
var name: String
var country: String
var use: String
var amount: Int
}
这种贷款结构与我们在上一节中定义的贷款结构非常相似,只不过它采用了codable 协议。您还应该注意,属性名与JSON数据的名称匹配.