Swift 開發 iOS 應用教程
蘋果最近宣布了一個改變iOS應用既往開發的一個大變化,一種取代Objective-C稱為Swift的完全不同的編程語言。我正在努力學習這門新語言,而且我決定將按照我的學習進程定期發布并共享我所找到的內容。這是眾多主題的第一篇文章,我希望你能跟隨下去!
下面的Swift代碼例子今后有極大的可能會改變。這主要是因為我開發風格是先寫代碼來測試理念,然后再重構,這主要是因為我(也可能是大家)對Swift完全是新手且正在學習。因此正如我過去學東西一樣,隨著時間的推移本教程也會隨之改變。我會根據需要更新的示例代碼,但沒有學習過程時這么多,驗證代碼也是如此。我覺得這樣也是個有幫助的過程。
所以我要從一個非常基本的應用開始,來解釋代碼如何工作。準備好了嗎?讓我們開始吧。
基礎
Swift沒有遵從標準的變量聲明模式——將變量類型放在變量名前,而是選擇類似JavaScript的‘var’關鍵字來定義所有變量。
比如你在Objective-C中這樣實例化:
NSString *myString = @"This is my string.";
你現在需要這樣:
var myString = "This is my string."
同時常數使用 ‘let’關鍵字來設定
let kSomeConstant = 40
在這種情況下kSomeConstant被隱式地定義為一個整數。如果你想更詳細也可以指定類型,像這樣:
對于數組和字典,使用方括號[]來描述
var colorsArray = ["Blue", "Red", "Green", "Yellow"]
var colorsDictionary = ["PrimaryColor":"Green", "SecondaryColor":"Red"]
其他還有很多,但是我認為這些基礎對作為教程的開始很重要。下面,讓我們開始“Hello World”。
Hello World
首先,我們會寫一個最簡單的起步應用,Hello World。
這個應用只做了一件事:在控制臺上打印出"Hello World"。你想要跟上技術前進的潮流的話,就需要下載XCode,這要求你有一個開發者賬號。如果你已經有了開發者賬號,那么就請前往http://devloper.apple.com ,獲取XCode。
至此,你已經建立好了集成開發環境。現在就可以向控制臺打印hello world了。這個例子不僅僅向你展示了可以完成構建的最簡單的應用,同時更為重要的是證明了你已經正確地建立了開發環境。
使用單一視圖應用模板建立Xcod項目,然后確定你選擇使用Swift語言。在項目的目錄樹里,現在你應當能夠看到文件AppDelegate.swift。 在這個文件里,你可以找到如下一行:
"http:// Override point for customization after application launch."使用下面神奇的hello world代碼替代上面這行:
println("Hello World")然后,按Run,你將會看到一個空白的應用啟動起來,接著在控制臺上打印出詞語"Hello World"。恭喜你,你已經用Swift編寫出第一個應用!雖然這個應用不可以獲得任何贊許,卻可以讓我們基于它進行更深入的探討。
添加表格視圖
在這一小節了,我們將真正地給屏幕上放置一些素材,非常好玩!
請先在Xcode里打開Main.storyboard文件,然后從對象庫拖動"Table View"對象。把這個對象放置在應用窗口里,并使它填滿整個屏幕,然后確保表格視圖與各個邊對齊。此時如果你運行此應用,你將在模擬器里看到的是一個空的表格視圖。
現在,我們需要給表格視圖設置delegate和數據源。使用接口構建器做這些工作非常簡單。只要一直選中表格視圖,然后點擊,并拖拉表格視圖到storyboard文件列表里的"視圖控制器“對象里。緊接著選擇”數據源“即可。類似的可以設置"delegate"。
好了,現在我們就可以更深入的看看表格視圖的通信協議的處理方法了。由于我們在視圖控制器里使用了UITableViewDataSource和
UITableViewDelegate,所以我們需要按照如下方式修改類的定義。
打開ViewController.swift,修改如下行:
class ViewController:UIViewController {
為
class ViewController:UIViewcontroller,UITableViewDataSource,UITableViewDelegate{
在上面兩個通信協議中的任何一個點擊+號就會在最前端顯示所有的函數。在當前的表格視圖里,我們至少需要兩個函數:
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
現在,我們修改視圖控制類,添加下面兩個函數:
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int { return 10 } func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! { let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyTestCell")cell.text = "Row #(indexPath.row)" cell.detailTextLabel.text = "Subtitle #(indexPath.row)"
return cell }</pre>
第一個函數是獲取表格所占用的行數,在這個例子里我們只是硬編碼其為10,然而通常這個值是表格視圖控制器數組的長度。現在例子里這么做純粹為了簡單。
第二個函數是發生變化的魔法所在。在這個函數里,我們創建了調用每個表格的UITableViewCell實例,而且標題風格為表格所帶風格。
接著,我們給每個表格賦予文本串"Row#\(indexPath.row)"。
在Swift里,就是這樣在字符串中嵌入變量的。現在我們正在做的事情就是通過在字符串中嵌入(\indexPath.row)來獲取indexPaht.row的值,可以用表格的行數動態的替代這個值。這樣就會產生下面結果:"Row #1","Row #2"等等。
這個詳細文本標簽之在子標題單元格中可用,就是我們在這使用的這個。我們把它設置為類似于“Subtitle #1″,“Subtitle #2″,等等。
繼續往下來,運行你的應用,你將看到一個讓人驚訝的單元格列表,它顯示有標題、副標題以及行數。這是在iOS中顯示數據最常用的方法,這必然會對你有所幫助。想看我視圖控制器文件完整的源代碼的話,看這里吧:ViewController.swif。
在第二部分中,我們將探討一個應用,這個應用使用iTunes的搜索API來發現并顯示iTunes商店里的專輯。
在第一部分中我們研究了一些Swift的基礎,并搭建了一個簡單的示例項目,創建了一個表視圖并在其中放置了一些文本。如果你沒讀過,到這里讀一下吧。
在本節中,我們要做一些更有抱負的事。我們將要接觸到iTunes商店的搜索API,下載JSON結果,將它們解析為字典然后用這些信息填充我們的Table視圖。然后,我們將為Table視圖添加單擊事件來添加一些用戶交互,當點擊一個項目后iTunes商店的相應項目將被打開。
這聽起來好像工作量很大,別擔心。這些都是iOS應用非常基本的功能,它也是每個開發者都要做的最普通的事情。那我們繼續吧。
連接到UI
我們需做的第一件事是得到一個Table試圖的引用,以便在在代碼中使用它。繼續往下走將下面這行添加到viewcontroller.swift文件中,放在類定義的下方,但要在任何函數之外。
<p>@IBOutlet var appsTableView : UITableView</p>
這段代碼可以連接我們分鏡頭中的Table試圖到“appsTableView”這個變量中。保存該文件并打開該分鏡。現在,通過ctrl鍵+單機+拖動,將Table視圖拖動到“View Controller”對象,這樣便完成了這些對象的連接。容易吧?
使用API進行網絡請求
現在我們已經實現了與UI的關聯,下面就準備進行API調用。創建一個名為searchItunesFor(searchTerm:String)的函數。我們用它來實現對任意搜索詞的網絡請求。
為了保持這邊文章簡短,我將只發布結尾處的代碼,并對部分注釋進行說明。我們總是在說明部分提出幾個問題,然后再做出進一步討論,因此話題范圍將非常廣泛!
func searchItunesFor(searchTerm: String) {// iTunes API要求使用+分隔多個搜索詞,因此在這兒用+號替代空格符 var itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)
// 現在轉義哪些無法識別出其是URL字符的其他字符 var escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) var urlPath = "
println("Search iTunes API at URL (url)")
connection.start() }</pre>
我們逐行看看上面代碼。
首先,我們需要對傳入搜索詞進行修正,搜索API要求搜索詞的格式為"第一個搜索詞+第二個搜索詞+第三個搜索詞+其他搜索詞",而不是"第一個搜索詞%20第二個搜索詞%20第三個搜索詞%20..."。因此,我們沒有調用URL編碼函數,而是調用了sringByReplacingOccurenesOfString的NSString方法。這個方法將返回搜索變量的修正版本,使用+號替代了其中的空格符。
接著,我們轉義了搜索詞中哪些無法識別為URL所包含的字符。
再接下來的兩行定義了NSURL對象,這個對象將做為iOS網絡API的URL參數。
以下兩行非常關鍵:
var request: NSURLRequest = NSURLRequest(URL: url) var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)第一行使用我們前面創建的url變量作為URL參數來創建NSURLRequest對象。接著第二行創建了即將真正用來發送請求的“連接”。注意:我們設置參數中的delegate為該對象自身。這樣我們就可以在視圖控制器類的內部偵聽連接發送來的響應信息。
最后,使用connection.start()發送請求。
接收響應之前要做的準備工作
現在我們已經有一個方法可以請求iTunes搜索結果了。這時,在viewDidLoad..的末尾處要插入下面這行:
searchItunesFor("JQ Software")這樣我們就可以在iTunes商店里查找哪些包含上面關鍵詞的任何軟件產品了,此時搜索的結果將包含幾年前我所編寫的兩個游戲和幾個較新的軟件。當然,你也可以按照自己的意愿更換搜索字符串。
接下來,為了能夠真正地接收到響應數據,我們需要跟蹤包含返回結果的數據對象。首先,給當前類里增加一個成員:NSMutableData實例,即在類的定義插入下面一行,這行位于大括號之內。要能接收數據,我們還需要創建一個用來存儲表格信息數組。
var data: NSMutableData = NSMutableData() var tableData: NSArray = NSArray()接下來,我們一起看看NSURLConnection發送我們編寫的類的函數,由于它是發送請求的代理,所以我們希望NSURLConnection發送的任何信息都能夠通過在NSURLConnectionDeatDelegate和NSURLConnectionDelegate里定義的通信方法回送。如果你還不明白,請不要著急。接著向下看,你將很快明白這些是如何運行的。
接受響應
現在我們將要添加目前為止最長的代碼塊了,不過依舊不是很長,用來處理整個的結果信息。
func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) { // Recieved a new request, clear out the data object self.data = NSMutableData() }func connection(connection: NSURLConnection!, didReceiveData data: NSData!) { // Append the recieved chunk of data to our data object self.data.appendData(data) }
func connectionDidFinishLoading(connection: NSURLConnection!) { // Request complete, self.data should now hold the resulting info // Convert the retrieved data in to an object through // JSON deserialization var err: NSError var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
if jsonResult.count>0 && jsonResult["results"].count>0 { var results: NSArray = jsonResult["results"] as NSArray self.tableData = results self.appsTableView.reloadData()
} }</pre>
當NSURLConnection 接受到一個響應,我們可能期望 didReceiveResponse方法代表我們被調用。此時我們可以簡單的重置我們的數據,通過 self.data = NSMutableData()來創建一個新的空的Data對象。
連接建立后,我們將開始在didReceiveData中接受數據。這里傳遞的數據參數是我們所有信息的來源。我們需要抓住傳遞進來的每一塊數據,因此我們將它添加到 self.data對象(我們之前清理過的)的結尾。
當連接最終完成并且所有數據都被接收到,connectionDidFinishLoading方法被調用并且我們可以在我們的應用中使用這些數據了,太棒了。
這里connectionDidFinishLoading方法使用NSJSONSerialization類通過反序列換來自iTune的結果來將我們的原始數據轉化為有用的字典對象。
現在,因為我們知道來自iTunes的數據格式略顯粗糙,因此一個簡單的對“result”鍵數量的檢查足以判斷是否是我們想要的數據了。所以我們現在可以設置我們的self.tableData對象為結果數據,并且告訴 appsTableView 去加載內容。這會引起Table View對象運行他自己的委托方法。定義這些是我們教程的最后一步了。
更新Table View UI
你可能還記得上一次我們為Table View完成了兩個功能。計數功能決定了行數; 單元格功能,真正的在每一行創建和修改單元格.
我們將要更新Table View使用我們從web下載的數據.
換出你的方法有以下兩個功能:
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyTestCell")var rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionary
cell.text = rowData["trackName"] as String
// Grab the artworkUrl60 key to get an image URL for the app's thumbnail
var urlString: NSString = rowData["artworkUrl60"] as NSString
var imgURL: NSURL = NSURL(string: urlString)// Download an NSData representation of the image at the URL
var imgData: NSData = NSData(contentsOfURL: imgURL)
cell.image = UIImage(data: imgData)// Get the formatted price string for display in the subtitle
var formattedPrice: NSString = rowData["formattedPrice"] as NSStringcell.detailTextLabel.text = formattedPrice
return cell
}
numberOfRowsInSection簡單的從tableData成員返回結果對象的數量, 并設置我們前面的connectionDidFinishLoading方法.在這個例子中cellForRowAtIndexPath也不會明顯的改變。 而是簡單的返回行數, 我們使用行數獲取三個信息: 曲目名稱, 作品的網址和價格.
使用這些關鍵字,我們在單元格中構建了標題,副標題和圖片。
嘗試運行我們的APP,你將第一次看到我們完全使用Swift真正創建了一些看起來像APP的東西。
為什么反應還是這么遲緩!?
在這個表格視圖里,我們沒能正確地處理許多事情。在接下來的三個小節里,我們將深入探討問題在哪兒和我們需要正確地做出那些修改。如果你愿意的話,可以跳到第五部分,在第五部分這些問題都得到了解決。
以后再第三部分,我們將關注的應用的交互部分,這樣用戶就可以搜索他要找的任何東西,而且可以點擊表格。
在第一部分和第二部分,我們研究的是Swift基礎余元部分,并建立了一個簡單的項目例子:創建表格視圖,把從iTunes搜索的API結果放入表格里。如果你還仍然還沒有閱讀這些內容,那么請查看第一部分和第二部分。
在這一小節里,我們先暫時停下來,然后清除目前狀況下我們所創建的部分代碼:刪除視圖控制器代碼里的網絡邏輯,然后修補一些影響性能的問題。做這些工作可能不是編寫一個新應用中最吸引人的地方,但卻非常重要!我們一起完成這些工作吧...
分離代碼
先說重要的, 讓我們用一些更有意義的名字來重命名我們的View Controller。打開你的 ViewController.swift文件并且將‘ViewController’ 替換為我們的新名字‘SearchResultViewController’。當然也要重命名文件為SearchResultViewController.swift。
如果你現在運行程序肯定會崩潰的。這是因為我們的storyboard文件還沒有被更新呢。因此打開Main.storyboard文件,在這個界面(左邊導航欄)選擇你的‘View Controller’對象,然后選擇身份檢查器(右邊,第三個按鈕)。
在這讓我們把‘ViewController’類名改為‘SearchResultViewController’。現在我們應該可以回到正軌了。檢查項目是否正常工作,然后我們繼續。
現在,我們將把類里的API代碼移到外部。在xcode導航面板按鼠標右鍵,然后選擇‘新建文件...'。這時就會進入iOS->代碼導航下的Cocoa Touch類。
這個文件將處理所有API的工作,因此我們稱它為APIController。
然后,我們選取searchItunesFor()函數和所有像connection(didRecieveResponse...)這樣的委托代理函數。把它們從Search controller剪切出來,然后黏貼到APIController里。你還需要拷貝哪些生成響應所需的數據變量。
如果你立刻進行構建,你會看到一下三個錯誤:
1) SearchResultsViewController當前沒有定義searchItunesFor();
2) APIController當前沒有定義self.tableData
3)APIController當前沒有定義self.appsTableView
為了解決以上問題,我們需要讓這些對象之間可以互相識別。因此要使APIController為SearchResultViewController的子對象。要做到這些非常容易,只要在SearchResultsViewController類定義的底部添加以下一行:
var api:APIController = APIController()然后修改調用searchItunesFor()哪行為:
api.searchItunesFor("Angry Birds")在這里,唯一不同點是通過APIController實例調用了這個方法,而不是一步完成所有的處理過程。
在要留心第一種錯誤同時,還需要APIController把結果返回給SearchResultViewController。我們希望API controller能夠響應任何API調用,因此我們還應當定義一個通信協議,視圖可通過這個通信協議獲取到響應。
定義API協議
引用tableData結果的APIController目前有兩行錯誤,我們暫時先刪除它們,使用更干凈清晰一些的東西。
在我們的APIController 類定義之上,我們添加一個聲明了待實現的didReceiveAPIResults函數的協議。
protocol APIControllerProtocol { func didRecieveAPIResults(results: NSDictionary) }他自己并不做任何事情,但是現在我們可以將這個協議添加到我們的SearchResultViewController中。現在不遵守協議就會引發錯誤,因此我們不會再犯沒有實現didReceiveAPIResults的低級錯誤了。
遵守協議
現在修改你的SearchResultsViewController來遵守協議:
class SearchResultsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, APIControllerProtocol {現在編譯應該會得到一個SearchResultsViewController沒有遵守 APIControllerProtocol協議的錯誤,很好!現在我們只需向我們的SearchResultsViewController類中添加函數就可以了。他看起來將非常像我們前面connectionDidFinishLoading類的內容。
func didRecieveAPIResults(results: NSDictionary) { // Store the results in our table data array if results.count>0 { self.tableData = results["results"] as NSArray self.appsTableView.reloadData() } }還有一件需要做的事情是改變我們的API controller使其包含一個委托對象,并且當連接完成一些API數據的加載后調用這個方法。
使用協議
返回APIController.swift文件,讓我們在類定義下增加一個委托。
var delegate: APIControllerProtocol?
結尾的問號表示委托是一個可選值。沒有問號,我們將獲得一個編譯錯誤,提示沒有為變量設置初始值,但是對我們來說是不會有這個錯誤提示的。可以在任何類中定義委托對象,只要它通過didRecieveAPIResults()方法附著到APIControllerProtocol中,因為我們在SearchResultsViewController已經完成上述工作。
現在我們已經添加了一個委托變量,讓我們返回到SearchResultsViewController,在方法viewDidLoad中,讓我們設置我們的api控制器的委托到自身,因此它可以接受委托函數的調用。
self.api.delegate = self
最后,在connectionDidFinishLoading方法中,讓我們移除tableView代碼并使用我們華麗的新協議方法替換它。
func connectionDidFinishLoading(connection: NSURLConnection!) {
// Request complete, self.data should now hold the resulting info
// Convert the retrieved data in to an object through JSON deserialization
var err: NSError
var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary// Now send the JSON result to our delegate object
delegate?.didRecieveAPIResults(jsonResult)
}吁,稍休息一下!
到現在為止,我知道所創建的應用都是些樣例應用,而且應用也確實像以前一樣可以做相同的事情,不過我們在許多方面都擁有了更多的靈活性。現在,我們使用APIController來實現對iTunes搜索API的調用,并讓一個定制的代理來接收響應。我認為目前我們把所有的時間都用在這些事情上,因此在第四部分我們將關注的是交互方面。如果你打算繼續向下閱讀,請通過email注冊一下。
這一小節中的全部代碼在這兒可以找到。
在第一、第二和第三部分,我們回顧了Swift的一些基礎知識,并創建了一個簡單示例項目:創建表格視圖,并把iTunes的API返回結果放到這些表格里。如果你到現在還沒有閱讀到這些內容,那么請閱讀第一、第二和第三部分。
對一些地方進行修改
好了,現在重中之重是幾個地方需要整理。代理函數cellForRowAtIndexPath里創建的表格單元視圖不是最有效的方法。在Obj-C里,如果內存里有一個表格單元可用,那么我們總是用dequeueResuableCellWithIdentifier來獲得這個表格單元,然不是在每次要用到表格單元的時候創建。這么做可以快速滾動,而且可以降低內存的使用。
因此,在SearchResultsViewcontroller.swift文件里,我們用下面語句替換了表格單元的實例化:
let kCellIdentifier: String = "SearchResultCell" var cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as UITableViewCell這么做將給出一個已經初始化的表格單元。在Swift里,要想知道SearchResultCell是什么,我們還需要在storyboard里聲明這個表格單元為原類型表格,并且設置這個表格的標識符為SearchResultCell。要做到這些,請打開storyboard,選擇表格視圖,然后更改"prototype cells"的數目為1,接著點擊這個表格單元。在屬性欄里,監視器會更改Style為"Subtitle",接著在標識符里鍵入"SearchResultCell"。
運行這個應用,我們再次看到完全相同的結果,不過這次在內存使用方面確實更高效,也確實最接近現實應用了!
讓表格單元“確實”可做某件事
現在,UTTableView可以通過代理類SearchResultsViewController調用多個代理函數了。其中有一種如下:
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!)無論何時點擊表格單元就會運行上面這個函數。這樣,我們就可以獲取所點擊的iTunes數據的行號:通過訪問由indexPath.row所設置的數組的索引來確定,即所點擊行的整型id。然后在彈出框內顯示同樣的信息。
如下增加didSelectRowAtIndexPath方法:
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) { // 獲取所選行的行數據 var rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionaryvar name: String = rowData["trackName"] as String var formattedPrice: String = rowData["formattedPrice"] as String
var alert: UIAlertView = UIAlertView() alert.title = name alert.message = formattedPrice alert.addButtonWithTitle("Ok") alert.show() }</pre>
這段代碼里,我們設置rowData的值為所選下標的數組對象的值,即第一時間把這個數組對象的信息放入到rowData里。然后根據rowData字典設置name和formattedPrice變量。接著,我們實例化了一個UIAlertView對象,并設置其title,message,并添加了一個取消按鈕。最后,我們用alert.show()顯示警告信息。
運行一下這個應用,現在你就應該能夠在彈出的窗口里看到所點擊應用的名字和價格了。很酷吧?
第四部分的代碼可以在這兒閱讀。
或者在這兒下載zip格式壓縮文件。
第五部分將集中加快表格視圖的顯示速度。想通過電子郵件獲得這個教程的最新版本,請在這而訂閱吧。
在第一到第四部分,我們研究的是Swift基礎部分,并建立了一個簡單的項目例子:創建表格視圖,把從iTunes搜索的API結果放入表格里。如果你還仍然還沒有閱讀這些內容,那么請閱讀第一部分。
表格有些慢!,我們要加快一下顯示速度。
現在,我們已經具有了所期望的功能,然而,如果你親自運行一下這個應用,你將看到這個應用超級慢!問題是:所有表格單元里的圖片都是在UI線程里下載的,而且是每次下載一個,而且這些圖片也根本沒有使用任何緩沖。接下來就修補一下這個問題。
一開始,在我們所創建的類里增加一個字典成員:
var imageCache = NSMutableDictionary()現在,我們需要對cellForRowAtIndexPath方法做更多修改。 這兒有這一方法的最終版本。這個鏈接會打開一個新的標簽頁,這么做可以讓后續的工作更容易些。
首先,對人們來說存在這樣的問題:使用API關鍵詞進行搜索造成結果混亂,這時人們獲取返回數據是他們不想得到的。因此在返回字典中增加一個對trackName值得檢查:
// 增加檢查,以確保有值 let cellText: String? = rowData["trackName"] as? String cell.text = cellText在下載要顯示的真實圖片之前,我們要確保給這個單元設置了用于占位的圖片。如果你想讓這個單元確實能包含圖像視圖,這么做就是必須的。否則以后裝載真實的圖片的時候就不會顯示!創建一個空白圖片(我用的是52x52像素,不過這無關緊要),然后,把這個圖片文件導入到你的項目:在finder里點擊并拖拉這個文件到Xcode項目,命名這個文件為Blank52,接著設置單元格使用這個圖片為占位圖片。你可以從 這兒獲取我創建的圖片文件。
cell.image = UIImage(named: "Blank52")現在,這個應用將很少出現閃退了,而且現在始終有一個圖片顯示在單元格里。
讓后臺線程下載圖片
接下來,我們打算啟動一后臺線程來進行圖片下載。Swift似乎沒有自己的GCD版本,因此,我們將使用Obj-C的API dispatch_async來進行圖片下載。
我們使用GCD API來開始這段代碼:
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DE這就允許可運行這段代碼之外的代碼,同時還可繼續在后臺下載圖像。這對一個交互式UI體驗尤其重要。
看看這段代碼,現在我們首先應當對圖片緩存進行檢查,看看以前是否下載過這張圖片。我們使用如下語句設置一個可選名字為image的名字變量。
var image: UIImage? = self.imageCache.valueForKey(urlString) as? UII如果緩存里沒有這張圖片(在初始化的時候,也不會存這張圖片),那么我們就需要下載它。有兩種方法可用來初始化下載。前面我們使用的是NSData的dataWithContentsOfFile,不過這兒我們將使用的是NSURLConnection的sendAsynchronousRequest,它更像我們使用API的方式。這么做的理由是:要真正快速地下載圖片,我們需要發送許多短小的請求。因此,我們就采用了這種方法。
我博客日志的這一部分可能沒有進行很好的排版,因此請打開gist,看看第35行。在這一行里我們所做的就是調用NSURLConnection的靜態方法sendAsynchronousRequest,這個方法將completionHandler函數當作參數。第36行道46行是這個函數的具體內容。
在這個函數里,我們將得到幾個返回變量: response,data和error。
在36行,我們將核對是否存在錯誤,如果沒有,將繼續向前執行到第38行:根據所給data創建UIImage。第37行事對前面方法的引用說明。
image = UIImage(data: data繼續向下倒第4行,我們設置圖片緩存使用這張圖片的URL作為名稱來保存新圖片。使用URL做名稱意味著在任何要顯示這張圖片的時候我們都可以在字典中找到它,甚至在完全不同的環境下也如此。
self.imageCache.setValue(image, forKey: urlString)最后,我們設置表格單元顯示圖片:
cell.image = image好了!運行一下這個項目,你就會看到非常漂亮的而且異常快速的新表格視圖!
當前可用的全部代碼在GitHub的'Part5'分支上。
你還可以在這兒下載zip格式的第五部分的代碼。
要想通過電子郵件獲取到這個教程的最新版,請在這兒訂閱。
第6部分將集中添加一個新的視圖控制器,我們可以打開這個控制器,然后把iTunes數據裝載進來。
在第一到第五部分,我們研究的是Swift基礎部分,并建立了一個簡單的項目例子:創建表格視圖,把從iTunes搜索的API結果放入表格里。如果你還仍然還沒有閱讀這些內容,那么請從第一部分開始閱讀。
如果你不愿意,那么你還可以從此處開始, 先下載第5部分的代碼。我們將把它作為范本開始后續說明。
在后續的教程里,我們將會做許多事情。
縮減API Controller部分的代碼
一開始,我們的目標是現實iTunes的音樂信息。因此,我們修改API controller,以便更好地處理這些信息。在我們做這些更改之前,我們需要簡化一下這個類。我們將使用在第五部分學到的sendAsynchronousRequest方法,然后再縮減API controller。要做到這些,我們需要刪除一下函數:
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) { func connection(connection: NSURLConnection!, didReceiveData data: NSData!) { func connectionDidFinishLoading(connection: NSURLConnection!) {還有,我們不再需要互斥的數據對象,因此也可以刪除:
var data: NSMutableData = NSMutableData()最后,我們刪除searchItunesFor()函數里的創建和發送請求部分的代碼:
var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)然后,使用以下代碼行替代:
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in if error? { println("ERROR: (error.localizedDescription)") } else { var error: NSError? let jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &error) as NSDictionary // Now send the JSON result to our delegate object if error? { println("HTTP Error: (error?.localizedDescription)") } else { println("Results recieved") self.delegate?.didRecieveAPIResults(jsonResult) } } })這兒,我們所做的就是刪除通信協議代理函數,取而代之的是選擇更多的使用函數型API sendAsynchronousRequest。這時,函數就會發送'request'對象參數給sendAsynchronousRequest,而在mainqueue里,則是另一個函數的句柄。當執行請求的時候,它就會調用內部的函數,這個內部函數將會檢查是否有錯誤,如果有,就把錯誤顯示在控制臺。如果沒有錯誤,那么它接著就會把結果轉換為JSON格式的NSDictionary。再通過JSON解析是否成功的檢查后,它就會把這個字典返回給代理。這兒有許多地方進行了代碼替換,而且在使用iOS開發所提供的更新的API時候,我們期望做這種更改。
我們在第三部分創建APIController這個類的時候,我們讓這個類繼承了NSObject,然而可以不這么做。因此我們修改這個類的定義如下:
class APIController {不過,我們做了這樣更改后,現在卻少了init方法!因此我們要增加一個,而且要確保這個方法里包括通信代理 。沒有代理的API Controller根本無法使用,因此為什么不編寫一個呢?
init(delegate: APIControllerProtocol?) { self.delegate = delegate }現在我們還需要對SearchResultsController進行一點修改。首先,我們需要把api變量改為可選的,這是因為我們不能把自己傳到初始化函數里。現在,我們把所有'api'聲明的地方替換為下面語句:
var api: APIController?這個語句表明我們聲明了一個名稱為api的對象,它的類型是APIController,而且其值可為空。
接著,我們需要在viewDidLoad里初始化這個api變量。
self.api = APIController(delegate: self)這樣就不必再有一行來設置通信代理了,現在init方法可以自動設置通信代理了。
創建iTunes專輯的Swift模型
我們還要修改通過關鍵字對音樂進行搜索的SearchItunesFor()調用,并對其進行調整,通過給其結尾處添加!強制它對api對象展開。我們能這么做事因為我們自己創建了api,而且我們知道有api對象存在。我們還顯示了一個networkActivityIndicator,用來告訴用戶有網絡操作進行。它將顯示在手機的頂端狀態欄里。
UIApplication.sharedApplication().networkActivityIndicatorVisible = true self.api!.searchItunesFor("Bob Dylan");然后看看APIController里的urlPath,我們修改專輯的API的參數如下:
var urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm非常棒!現在我們編寫的APIController更加簡潔,代碼更少!
我們需要一個模型!
為了方便地傳遞專輯信息,我們應當創建一個表示專輯的模型.創建一個新的swift文件,命名為Album.swift,內容如下:
class Album { var title: String? var price: String? var thumbnailImageURL: String? var largeImageURL: String? var itemURL: String? var artistURL: String?init(name: String!, price: String!, thumbnailImageURL: String!, largeImageURL: String!, itemURL: String!, artistURL: String!) { self.title = name self.price = price self.thumbnailImageURL = thumbnailImageURL self.largeImageURL = largeImageURL self.itemURL = itemURL self.artistURL = artistURL } }</pre>
這是一個非常簡單的類,它只為我們提供了專輯的幾個屬性。我們創建了類型為可選字符串的6個不同的屬性,增加了一個在使用這個對象之前對其展開的初始化方法。 初始化方法非常簡單,它僅僅依據所提供參數設置所有屬性。
現在,我們編寫了一個專輯對象類,下面就可以使用了。
使用新的專輯swift模型
回頭看看SearchResultController,我們刪除了NSArray類型的數組變量tableData,而選擇使用表示專輯的Swift數組。在Swift里,做到這些非常簡單:
var albums: Album[] = []這個語句將創建一個可存儲專輯的空數組。現在我們需要更改解析專輯的tableView的dataSource和通信代理方法。
在numberOfRowsInSection方法里,我們更改元素數為專輯數組里包含的專輯數:
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int { return albums.count }然后,在cellForRowAtIndexPath方法里,我們用單個專輯查詢方法替代了字典查詢方法:
let album = self.albums[indexPath.row] cell.text = album.title cell.image = UIImage(named: "Blank52") cell.detailTextLabel.text = album.price稍后,在獲取縮略圖的地方,我們用下面語句替換了urlString設置器:
let urlString = album.thumbnailImageURL你們中的某些人可能已經注意到圖像顯示有問題,而且彼此覆蓋。出現這種情況是因為對表格單元重用引起的。我們修改了代碼,以檢查sendAsynchronousRequest函數里可看到的表格單元是否存在(即是否可見)。修改的代碼段很大,請在 整個函數的gist了查閱。
根據JSON創建專輯對象
現在,如果我們沒有第一時間創建專輯信息,那么上面所做的更改將無法使用。我們需要修改didRecieveAPIResults方法,以獲取返回的專輯信息,這些專輯信息是根據返回的JSON響應來創建的,接著還要把這些專輯信息存儲到專輯數組里。修改
后的這個方法的最終版如下:
func didRecieveAPIResults(results: NSDictionary) { // 把結果存儲到表格數據數組里 if results.count>0 { let allResults: NSDictionary[] = results["results"] as NSDictionary[]// 有時候,iTunes返回的是一個集合,而不是音軌,因此我們要核查這兩個name值 for result: NSDictionary in allResults { var name: String? = result["trackName"] as? String if !name? { name = result["collectionName"] as? String } // 有時候價格是formattedPrice, 有時是collectionPrice..還有的時候是浮點數,而不是字符串,哎呀,什么情況都有! var price: String? = result["formattedPrice"] as? String if !price? { price = result["collectionPrice"] as? String if !price? { var priceFloat: Float? = result["collectionPrice"] as? Float var nf: NSNumberFormatter = NSNumberFormatter() nf.maximumFractionDigits = 2; if priceFloat? { price = "$"+nf.stringFromNumber(priceFloat) } } } let thumbnailURL: String? = result["artworkUrl60"] as? String let imageURL: String? = result["artworkUrl100"] as? String let artistURL: String? = result["artistViewUrl"] as? String var itemURL: String? = result["collectionViewUrl"] as? String if !itemURL? { itemURL = result["trackViewUrl"] as? String } var newAlbum = Album(name: name!, price: price!, thumbnailImageURL: thumbnailURL!, largeImageURL: imageURL!, itemURL: itemURL!, artistURL: artistURL!) albums.append(newAlbum) } self.appsTableView.reloadData() UIApplication.sharedApplication().networkActivityIndicatorVisible = false } }</pre>
這段代碼似乎存在大量的新代碼,不過這段代碼卻非常簡單。
首先,我們從API的返回結果提取results關鍵字的內容到NSDictionary數組allResults,它包含著所有專輯信息。
let allResults: NSDictionary[] = results["results"] as NSDictiona然后,我們使用Swift的for-each語法對allResults的每個內嵌的字典進行輪詢查找,并把每個元素賦值給臨時變量result。
for result: NSDictionary in allResults {接下來,你將看到許多如下的語句:
var name: String? = result["trackName"] as? String if !name? { name = result["collectionName"] as? String }這里發生的是iTunes對歌曲和專輯使用不同的鍵。因此所有這些域應該聲明為可選的,并且我們應該檢查它們至少它們其中一個被保存。如果你選擇使用不同的媒體類型,這對于你的應用會是非常重要的。
最終格式化我們所有信息和驗證它們都存在后,我們創建一個新的唱片集,并且添加到我們的唱片集數組中來:
var newAlbum = Album(name: name!, price: price!, thumbnailImageURL: thumbnailURL!, largeImageURL: imageURL!, itemURL: itemURL!, artistURL: artistURL!) albums.append(newAlbum)既然都處理完了,我們重新加載我們的表視圖,然后關掉網絡監視器。
self.appsTableView.reloadData() UIApplication.sharedApplication().networkActivityIndicatorVisible = false創建第二個視圖
現在需要顯示一個唱片集的詳細信息了,我們需要一個新的視圖。首先我們創建這個類。添加一個叫做DetailsViewController.swift并繼承自UIViewController的文件。
我們的視圖控制器將非常簡單。我們只需添加一個album,然后實現UIViewController的init和viewDidLoad方法即可。
import UIKitclass DetailsViewController: UIViewController {
var album: Album?
init(coder aDecoder: NSCoder!) { super.init(coder: aDecoder) }
override func viewDidLoad() { super.viewDidLoad() } }</pre>
這段代碼沒有做多少,但是沒關系。我們需要這個類以便設置我們的storyboard。
既然我們會在棧中向后向前推視圖,那我們一定需要一個導航欄。很難用文字解釋,并且有保密協議不允許我展示Xcode 6截圖,因此取而代之,我創建了一個短視頻來展示怎樣在Xcode 5中做這些。這一個過程跟在Xcode 6 Beta 中幾乎一樣,并且不受保密協議的約束。
在這段視頻里,我們完成了以下工作:
使用編輯菜單中的Xcode快捷鍵在控制器導航里嵌入了我們創建的視圖控制器:點擊試圖控制器,然后選擇編輯-〉嵌入-〉控制器導航
添加了新的視圖控制器
設置DetailsViewController對應的類和storyboard ID。
把我們最先創建的視圖控制器里的表格單元視圖通過Ctrl、點擊加上拖拉到我們剛創建的心得視圖控制器里,接著選擇segue類型為'push'。
最后一步所做的事情就是在控制器導航上創建一個segue,它將新的視圖放在堆棧的最頂端。此時運行這個一下這個應用,然后點擊單元格,你將能夠看到動態載入了新的視圖。
接著我們給新的視圖增加一個簡單的UI。這個UI將包括100x100像素的UIImageView,一個標題,一個按鈕和一個文本視圖。從對象庫中拖拉出上面所說的所有對象,然后在新的視圖上按照自己的意愿對這些對象進行布局。在創建這個UI之前,請注意:XCode6有一個顯示尺寸分類的概念。為了達到我們的目標,我們需要設置顯示尺寸分類為iPhone。這時在顯示窗口的底部,你將可以看到“寬多少,高多少”這樣的描述。這就是當前storyboard的大小。如果你奇怪為什么你所創建的視圖是正方形的,這就是原因所在。點擊它,更改它以顯示iPhone大小圖片,即設置其為"寬度壓縮|高度任意“。
給專輯信息提供新的視圖
當啟動storyboard里的segue的時候,它首先調用的時當前屏幕的視圖控制器所對應的名字為prepareForSegue函數。我們將攔截這個函數調用,通過這種方式來告訴新的視圖控制器我們正在查看哪個專輯。向SearchResultsViewController添加以下代碼:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject) { var detailsViewController: DetailsViewController = segue.destinationViewController as DetailsViewController var albumIndex = appsTableView.indexPathForSelectedRow().row var selectedAlbum = self.albums[albumIndex] detailsViewController.album = selectedAlbum }這兒傳入的segue參數具9有一個名字為destinationViewController的成員,它就是我們剛剛創建的DetailsViewController。為了設置專輯信息,我們首先需要使用上面代碼中所示的'as'關鍵字把它轉換為DetailsViewController。
接著,我們通過調用表格視圖的indexPathForSelectedRow()方法確定運行此segue的時選擇的是哪一專輯。
有了這些信息,我們就可以告訴detailsViewController在顯示專輯信息之前我們點擊了哪一專輯。
接下來,我們將會向你展示Xcode的一個非常強大的特性。通過使用這個特性,Xcode將會替我們編寫部分代碼。
再次打開storyboard,開始給圖像視圖、標簽、按鈕和文本視圖創建對應的IBOutlet。在Xcode的右上角有一個"輔助“按鈕。它的圖標像一個帶有領結的男外套。點擊一下將會在storyboard窗口的右下方開啟一個代碼窗口。看一下是否其中一個面板顯示的DetailsViewController.swift,另一個顯示的是Main.storyboard。
然后,按住ctrl,接著點擊并拖拉圖像視圖到代碼文件。這時DetailsViewController類的定義下方就會出現一條橫線。提示你輸入名稱,我們輸入"albumCover"。選項就選默認的就可以了。做完這些操作后,你就會看到新增了下面這行代碼:
@IBOutlet var albumCover : UIImageView此時,我們創建了一個新的IBOutlet,并且把它與storyboard里的DetailsViewControll關聯起來。非常酷吧?
然后對你添加到視圖的其他對象也做同樣的操作。完成這些以后,我們還要修改viewDidLoad,以便裝載傳遞給視圖對象的信息。下面是DetailsViewController代碼的最終版本:
import UIKitclass DetailsViewController: UIViewController { @IBOutlet var albumCover : UIImageView @IBOutlet var titleLabel : UILabel @IBOutlet var detailsTextView : UITextView @IBOutlet var openButton : UIButton var album: Album? init(coder aDecoder: NSCoder!) { super.init(coder: aDecoder) } override func viewDidLoad() { super.viewDidLoad() titleLabel.text = self.album?.title albumCover.image = UIImage(data: NSData(contentsOfURL: NSURL(string: self.album?.largeImageURL))) }
}</pre>
上面的幾個@IBOutlet是由storyboard創建的UI與視圖控制器之間的關聯,而viewDidLoad方法則根據裝載的專輯對象設置專輯標題和專輯封面。
現在,試著運行應用程序并看一下結果。我們現在可以深入探究相冊的詳細信息,并且得到一個與專輯封面和標題相關的詳細信息視圖。因為我們之前做了導入導航控制器的操作,因此我們還免費獲得功能后退按鈕 !
如果目前為止,你已經做到這些,請在 我的推ter上面告訴我你已經成功完成,因為我想要親自祝賀你!你在用Swift開發iOS應用的道路上進展不錯。
下一次,我們要弄清楚一些需要放在我們大段文本字段中的東西,并且我們要讓我們的按鈕能在展示專輯時做一些酷酷的事情。 所以一定要注冊,以便下一次通知。
這一節的完整源代碼可以 在這里獲得