概述
摘要:用JSON和工具栏做一个分析白宫请愿书的app
概念:JSON,NSData,UITabBarController
1.设置
2.创建基本UI:UITabBarController
3.JSON解析:NSData 和SwiftyJSON
4.演绎一个请求:loadHTMLString
5.结束接触:didFinishLaunching
6.总结
设置
这个项目会从网站获取数据并为用户整理有用的消息。按照以往的管理,这就是教你一些新的iOS开发技术的方法,但让我们面对现实——你已经完成了2个app和2个游戏了,所以你已经开始建立起一个很好的工作库!
这次,你会学习UITabBarController,NSData等内容。你也会使用一种叫JSON的数据格式,它是一种很流行的线上收发数据的方式。要发现一些有趣的JSON免费订阅服务不太容易,但但我们要获取的是“We the people”美国白宫的请愿书,在这里美国人可以提交要求某些行动的发生,而其他人可以对它进行投票。
有些是完全不重要的,但它也有好的,干净的JSON可以让每个人阅读,这让它成为我们的订阅源变得十分合适。很多要学,很多要做,所以,让我们开始吧:在Xcode中创建一个新项目,选择Master-Detail Application 模板,命名为project7并保存。
原文链接:
https://www.hackingwithswift.com/read/7/1/setting-up
创建基本UI:UITabBarController
目标给了我们很多不需要的东西,但这次我们不是要删除而是调整。在用户界面上我们只需要做一点调整,所以现在在IB中打开Main.storyboard。
在故事板中,你会看到有两个导航控制器:上下各一。选中上面的那个,然后选择菜单Editor>Embed In>Tab Bar Controller。跟UINavigationController一样,UITabBarController也是个iOS用户界面设计中常用的元素。标签栏是底部的一条图标带,它可以显示很多屏幕的内容,在App Store,音乐,电话等app中都可以看到。
在屏幕背后,UITabBarController管理着可供用户选择的一组视图控制器。你确实可以经常在IB中完成很多工作,但不是在这个项目中。我们要用一个标签来显示近期请愿,另一个显示热门请愿,他俩其实是一样的——只是数据来源不同而已。
在故事板中完成全部任务意味着要复制我们的视图控制器,这是个糟糕的主意,所以我们要做的正相反,我们要设计一个标签的内容,然后用代码复制一份。
现在我们的导航控制器已经在一个标签栏控制器里了,在IB中它的底部带有一条灰色带子。如果你现在点击它,它会选中一个叫UITabBarItem的新类型的对象,也就是早标签栏中显示视图控制器的图标和文字。在属性观察器中把System Item从“Custom”改成“Most Recent”。
关于UITabBarItem有一件很重要的事情是当你设置好它的System Item,它会同时给标签的标题以图标和标题。如果你尝试把文本改成你自己的内容,图标就会被移除而你也需要提供自己的。这是因为Apple已经让用户形成“某些图标就是代表某些信息”的习惯,而他们不想你用一种不正确的方式来使用这些图标!
选中导航控制器本身(直接点击Navigation Controller的文字),然后按下Alt+Cmd+3来打开身份观察器。我们没进过这里,因为不是那么常用。但这里,我要你做的是在Storyboard ID这一项右边的空格中输入NavController。我们很快就会用到了!
另外一个改动是在表视图控制器中。点击Title(就在Prototype Cells下面),然后在属性观察器中你会看到一些关于cell的选项。在属性观察器的顶部显示“Table View Cell”表示你选对了对象。把style从“Basic”改成“Subtitle”,它会在标题下面添加一个副标题。
IB部分搞定了,现在打开MasterViewController.swift文件来做一些基础性改动。首先,删除整个insertNewObject()方法,删除viewDidLoad()中除了super.viewDidLoad()之外的全部内容,删除表视图的commitEditingStyle 和 canEditRowAtIndexPath方法,最后删除prepareForSegue()和cellForRowAtIndexPath方法中的as! NSDate——不是一整行,而只是as! NSDate。
第一步完成了:我们有了一个基本的用户界面,而且我们已经清理了模板中的不想要的烦人东西。现在开始码代码……
原文链接:
https://www.hackingwithswift.com/read/7/2/creating-the-basic-ui-uitabbarcontroller
JSON解析:NSData 和SwiftyJSON
JSON——JavaScript Object Notation的缩写——是一种描述数据的方式。它不是读取自己的最简单方式,但它简单紧凑,电脑很容易分析,所以它在带宽还很窄的时候它就在网上很流行了。
项目6中你已经学过在Auto Layout中使用字典,在这个项目中我们会在更大范围内使用字典。甚至,我们还会把字典放进数组,这样就能保证数据按顺序排列。
你用方括号定义一个字典,然后输入它的key类型,一个冒号,再是它的value类型。比如一个key为字符串,UILabel为值的字典的定义应该如下所示:
var labels = [String: UILabel]()
定义数组时,我们用的是:
var strings = [String]()
所以把这俩放到一起就是我们想要的字典数组,每个字典都有一个字符串关键字和一个字符串值。所以,它看起来是介样:
var objects = [[String: String]]()
把它放在MasterViewController.swift的顶部的objects定义的位置上,现在的类型为AnyObjects,这完全没用。
是时候分析一些JSON了,这意味着要处理它并检查它的内容。在Swift中并不容易,所以出现了一系列的辅助库,用它们就可以帮你完成很多的重任。我们将要用到其中的一部分:从GitHub中下载项目文件然后找到一个叫SwiftyJSON.swift的文件。把它添加进你的项目,确保“Copy items if needed”已经勾选。
SwiftyJSON让我们可以用一种非常接近直觉的方式读取JSON:你可以把所有的东西都当成字典来对待,所以如果你知道有个值叫“information”,它包含另外一个值“name”,而它又包含另外一个值“firstName”,你可以使用json["information"]["name"]["firstName"]来获得数据,然后使用string属性来向Swifty请求获取它。
在我们分析之前,这里是你接收到的实际JSON中极小的一部分:
{
"metadata":{
"responseInfo":{
"status":200,
"developerMessage":"OK",
}
},
"results":[
{
"title":"Legal immigrants should get freedom before undocumented immigrants – moral, just and fair",
"body":"I am petitioning President Obama's Administration to take a humane view of the plight of legal immigrants. Specifically, legal immigrants in Employment Based (EB) category. I believe, such immigrants were short changed in the recently announced reforms via Executive Action (EA), which was otherwise long due and a welcome announcement.",
"issues":[
{
"id":"28",
"name":"Human Rights"
},
{
"id":"29",
"name":"Immigration"
}
],
"signatureThreshold":100000,
"signatureCount":267,
"signaturesNeeded":99733,
},
{
"title":"National database for police shootings.",
"body":"There is no reliable national data on how many people are shot by police officers each year. In signing this petition, I am urging the President to bring an end to this absence of visibility by creating a federally controlled, publicly accessible database of officer-involved shootings.",
"issues":[
{
"id":"28",
"name":"Human Rights"
}
],
"signatureThreshold":100000,
"signatureCount":17453,
"signaturesNeeded":82547,
}
]
}
实际上你会得到大约2000-3000行这样的东西,全都是来自美国公民,关于政治分类下的请愿。请愿的内容跟我们没啥关系,我们关心的是它的数据结构。特别是:
1. 这是个元数据值,它包含了一个responseInfo值,responseInfo再包含了一个状态值。状态200是网络开发者用来表示“一切OK”的意思。
2.这里有个结果值,它包含了一系列的请愿。
3.每个请愿包含一个标题,一个正文,一些相关事件,还有一些特征信息。
4.JSON也有字符串和整型。注意字符串都是在引号中被使用,而整型不需要。
现在你对JSON的工作方式有了基本的了解,是时候该写点代码了。我们将要升级viewDidLoad()方法这样它就能从WhiteHouse请愿系统中下载数据,然后把它转化成SwiftyJSON对象,同时确认状态值等于200。
我们将用NSURL和一个新的NS类NSData来完成这件事。NSData类是被设计用来存放任何格式的数据,可能是字符串,可能是图像,又可能是其他的什么东西。你已经见过用contentsOfFile从硬盘载入数据可以创建NSString。contentsOfURL可以从URL(一定要用NSURL)下载数据,然后创建NSData(还有NSString)。
下面是新的viewDidLoad()方法:
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "https://api.whitehouse.gov/v1/petitions.json?limit=100"
if let url = NSURL(string: urlString) {
if let data = try? NSData(contentsOfURL: url, options: []) {
let json = JSON(data: data)
if json["metadata"]["responseInfo"]["status"].intValue == 200 {
}
}
}
}
让我们关注下新内容:
URLString指向Whitehouse.gov服务器,连接请愿系统。
我们用if/let来确保NSURL是可用的,而不是强制解包。晚点你可以回过来添加更多的链接,所以这样做更安全。
我们用contentsOfURL方法创建了一个新的NSData对象。它从NSURL返回了内容,也就是我们所需的——这就是我们使用[]作为选项的原因。它可能会抛出一个错误,所以我们要用try?.
如果NSData对象被成功创建,我们就从它创建一个新JSON对象。这就是个SwiftyJSON结构体。
最后,我们有了JSON解析的第一位:如果存在“metadata”值,它包含的“responseInfo”值也包含的“status”值,将其作为整型返回,然后跟200进行比较。
“we're OK to parse!”以“//”开头,表示Swift中的评论。评论通常会被编译器忽视;我们写它是给自己看的。
SwiftyJSON擅长JSON解析的理由是它的核心内建了可选性。如果任何“metadata”、“responseInfo”、“status”不存在,它就会返回0给status——我们不需要每个都单独检查。如果你在读取一个字符串的值,SwiftyJSON会返回它找到的字符串,或者一个空字符串,如果不存在的话。
代码并不完美,还远着呢。实际上,viewDidLoad()从互联网上下载数据时,我们的app会在数据转移完全之前都会被锁定。有解决办法,但为了避免过于复杂它们会在项目9中出现。
现在,让我们集中关注JSON解析。我们已经为数据字典准备了一个objects数组。我们想要把解析过的JSON存入字典中,每个字典都有三个值:请愿的标题,正文,支持票数。一旦存储完成,我们需要告诉表视图让它重新载入自身。
准备好了?因为比起它要完成的工作量来说,代码真的是出奇的简单:
func parseJSON(json: JSON) {
for result in json["results"].arrayValue {
let title = result["title"].stringValue
let body = result["body"].stringValue
let sigs = result["signatureCount"].stringValue
let obj = ["title": title, "body": body, "sigs": sigs]
objects.append(obj)
}
tableView.reloadData()
}
把这个方法直接放到viewDidLoad()方法下面,然后把viewDidLoad()中的//we're OK to parse!用这个替代:
parseJSON(json)
方法parseJSON()从它得到的JSON对象读取数组“result”。如果你回顾一下我给你的JSON代码片段,results数组包含所有的请愿等待读取。当你通过SwiftyJSON使用arrayValue时,你会返回一个有对象的数组或者空数组,所以我们在循环中使用返回值。
对于结果数组中的每个结果,我们需要读出三个值:标题,正文,还有支持签名数,所有都是以字符串的格式获取。签名数从JSON中得到时实际是数字,但SwiftyJSON为我们把它转换成字符串是因为我们的字典中key和value都是字符串。
每次我们用stringValue来访问result中的一个元素,我们会得到它的值或者空字符串。无论如何,我们都会得到点什么,所以我们从所有三种值构建一个新的字典,然后用objects.append()来把新的字典加入到我们的数组中。
一旦所有的结构都被解析完毕,我们会告诉表视图重新载入,这样代码就完成了。
好了,如果Whitehouse实际使用的是好的HTTPS,那么我们的app就已经完成了。虽然我们要获取的URL的开头是https://api.whitehouse.gov,在写的时候HTTPS形式非常弱所以iOS9并不信任它。所以,如果你尝试运行代码你会得到一个错误提示:“NSURLSession/NSURLConnection HTTP load failed(kCFStreamErrorDomainSSL, -9802)”。(NSURLSession/NSURLConnection HTTP载入失败)
解决办法是给Whitehouse的连接安全度升级。我们可以请求iOS允许这个网站主机作为例外处理,办法是修改它的App Transport Security Settings(应用程序传输安全设置)。这是个烦人的东西,我本来不想给你看,除非是万不得已。我恐怕现在就是。
所以,在项目导航中找到一个名为Info.plist的文件。右击它然后选择Open As>Source Code。它的结尾是介样的:
</dict>
</plist>
在此之前,我想让你加入下面的代码:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>whitehouse.gov</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSThirdPartyExceptionAllowsInsecureHTTPLoads<key>
<true/>
</dict>
</dict>
</dict>
以上代码增加了一个App Transport Security例外意思是iOS不会因为WhiteHouse的弱证书而拒绝处理它的数据。
现在你可以运行程序,虽然它目前还不是最理想的:你会看到一行行奇怪的文字格式。这是因为Xcode项目在表视图的cellForRowAtIndexPath方法中内建了如下代码:
cell.textLabel!.text = object.description
单元格的文本标签期待的是一个字符串,而不是字典,但Xcode的默认模板代码用object.description要让object以字符串的形式被描述。换做是字典,打印出来的就是一组漂亮的key/value布局,里面有字典的所有内容。
我们想把它调整成输出字典的title值,而且我们还要用到被从基础改成副标题的单元格中的副标题文本标签。把现在的代码(上面的一行)改成:
cell.textLabel!.text = object["title"]
cell.detailTextLabel!.text = object["body"]
我们已经给字典里的title,body和sigs键都赋了值,现在我们可以读取它们来配置单元格了。
如果现在运行,你会看到该有的都有了——每个单元格现在显示了请愿标题还有下面的部分正文内容。空间不足时,副标题最后默认显示“...”,是时候该给用户看点现在的热门了。
原文链接:
https://www.hackingwithswift.com/read/7/3/parsing-json-nsdata-and-swiftyjson
演绎一个请求:loadHTMLString
JSON解析完成之后就简单了:我们要升级DetailViewController类这样提取请愿内容时会更加漂亮。最简单的从网站演绎复杂内容的方法差不多总是要用到WKWebView,所以我们也会在这里用到跟项目4一样的技术来调整DetailViewController,来让它拥有一个网页视图。
把DetailViewController的所有内容用下面的替代:
import UIKit
import WebKit
class DetailViewController: UIViewController {
var webView: WKWebView!
var detailItem: [String: String]!
override func loadView() {
webView= WKWebView()
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
这跟项目4的代码几乎一样,但你应该已经注意到我增加了一个detailItem属性来存放我们的字典数据。
这部分比较简单,难的是我们不能直接把请愿文本内容直接放进网络视图,因为这样会让显示变得细长。相反,我们得用HTML来解析代码。HTML是另一门全新的语言有它自己的规则和复杂性。但这个系列不叫“Hacking with HTML”,所以我不打算讲很多HTML的细节问题。但我会说我们要用的HTML会告诉iOS让页面适应设备,而且我们希望字体大小为标准的150%。所有这些HTML会跟我们字典的body值连在一起送入网络视图。
把下面的代码放入viewDidLoad()中super.viewDidLoad()下面:
guard detailItem != nil else { return }
if let body = detailItem["body"] {
var html = "<html>"
html += "<head>"
html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
html += "<style> body { font-size: 150%; } </style>"
html += "</head>"
html += "<body>"
html += body
html += "</body>"
html += "</html>"
webView.loadHTMLString(html, baseURL: nil)
}
这里有个重要的Swift新语句:guard。它的目的是创建“提早返回”,就是说在你码完代码以后,如果它发现重要数据丢失就会直接跳出不再继续执行代码。这里我们希望在detailItem被设置之前程序不会运行,此时guard就会执行return。
我已经试着把HTML尽可能的讲清楚,但如果你觉得无所谓也不要紧。重要的是Swift有个叫html的字符串用来存放显示页面所需的全部代码,通过方法loadHTMLString()来完成。这跟之前载入HTML的方法不同,因为我们不需要载入整个网络,而只是一部分。
详情视图控制器部分就到这里了,真的挺简单。好了,试试新完成的程序吧。
原文链接:
https://www.hackingwithswift.com/read/7/4/rendering-a-petition-loadhtmlstring
结束接触:didFinishLaunching
在结束之前,我们还要做两个小改动。第一,我们要给UITabBarController增加一个显示热门请愿的标签,其次,我们要通过增加错误提示来让NSData载入过程更加人性化。
我之前就说过,我们不能直接把第二个标签放进故事板因为两个标签都持有一个MasterViewController,而这样的话就要求我们要在故事板里面复制一个视图控制器。你可以这样做,但请不要这样——它简直就是个梦靥!
相反,我们要做的是不改动故事板而是用代码来创建第二个视图控制器。这是之前没做过的事,但它并不困难,而且我们已经踏出第一步了。
打开AppDelegate.swift。它一直在我们的项目中,但是我们从未使用过。在文件的顶部找到didFinishLaunching。当app准备运行时它会被调用,而我们准备通过改动它来达到在标签栏插入第二个MasterViewController的目的。
方法中已经有些代码在了,但我们还要在return true之前加入更多:
let tabbarController = splitViewController.viewControllers[0] as! UITabBarController
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIndentifier("NavController") as! UINavigationController
vc.tabBarItem = UITabBarItem(tabBarSystemItem: .TopRated, tag: 1)
tabBarController.viewControllers?.append(vc)
几乎每行都是新的,所以让我们深挖下:
我们的故事板在我们的视图控制器显示的地方自动创建了一个新窗口。这个窗口需要知道它的初始视图控制器是哪个,然后把它赋值给rootViewController属性。这些全都是由故事板完成的。
在Master-Detail Application 模板中,根视图控制器是UISplitViewController,它有一个属性叫viewControllers,用于储存两个项目:第一个是左边的视图控制器(即表视图),第二个是右边的(详情视图)。
我们需要手动创建一个新的MasterViewController,第一种方法是引用Main.storyboard文件。可以用类UIStoryboard完成。你不需要提供目录,因为nil表示使用默认目录。
我们用名字超长的方法instantiateViewControllerWithIdentifer()创建视图控制器,以我们想要的视图控制器的故事板ID作为参数传入。早些时候我们给导航控制器设置ID为“NavController”,所以我们使用它作为参数,并将结果的类型转换为UINavigationController。
我们为新的视图控制器创建了一个新的UITabBarItem对象,给它“Top Rated”图标和tag1。标签很重要,但不是现在。
我们把心的视图控制器加入到我们的标签栏控制器数组viewController中,它会在标签栏中显示新的标签。