概述
摘要:用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中,它會在標簽欄中顯示新的標簽。