
There is no learning without trying lots of ideas and failing lots of times.
- Jonathan Ive
到目前為止,我們僅僅專注于在一個 table view 里顯示數(shù)據(jù)。我猜你應(yīng)該在想我們怎樣才能與 table view 互動和檢測行選擇的。這是我們這章將要討論的。
我們將繼續(xù)改進我們在之前章節(jié)構(gòu)建的 FoodPin app,添加一些增強功能:
- 當用戶按單元格的時候彈出一個菜單。這個菜單提供兩個選項:Call和I’ve been here。
- 當用戶選擇“I’ve been here”時顯示一個心形的圖標。
通過實施這些新功能,你還將學(xué)習(xí)如何使用 UIAlertController,它通常用于在 iOS apps 里顯示警告。

理解 UITableViewDelegate 協(xié)議
當我們在第八章首次構(gòu)建 SimpleTable app,我們添加了2個委托:UITableViewDelegate 和 UITableViewDataSource,到 RestaurantTableViewController 類里。我已經(jīng)和你們討論過 UITableViewDataSource 協(xié)議但是僅僅提到了 UITableViewDelegate 協(xié)議。
像之前說的,委托模式在 iOS 編程里是非常常見的。每個委托負責一個特定角色或者任務(wù)來保持系統(tǒng)簡單和干凈。當一個對象需要執(zhí)行特定的任務(wù)時,它依賴于另一個對象來處理它。這在軟件設(shè)計里通常被稱作“關(guān)注點分離(separation of concerns)”。
UITableView 類提供這個設(shè)計概念。這兩個協(xié)議為了不同的目的而設(shè)計。UITableViewDataSource 協(xié)議定義方法,被用來管理表格數(shù)據(jù)。它依賴于委托(delegate)所提供的表格數(shù)據(jù)(table data)。另一方面,UITableViewDelegate 協(xié)議負責設(shè)置table view的頁眉和頁腳部分,同時也處理單元格選擇和單元格重新排序。
為了管理行選擇(row selection),我們將在 UITableViewDelegate 協(xié)議里執(zhí)行一些方法。
閱讀文檔
在執(zhí)行方法之前,你可能想知道:
我們?nèi)绾沃?UITableViewDelegate 里哪個方法將被執(zhí)行呢?
答案就是“閱讀文檔”。你已經(jīng)獲得了免費訪問蘋果官方 iOS 開發(fā)者文檔(https://developer.apple.com/library/ios/)的權(quán)利。作為 iOS 開發(fā)者,你需要適應(yīng)閱讀 API 文檔。地球上沒有一本書能覆蓋關(guān)于 iOS SDK 的所有事情。大多數(shù)時間當我們想要學(xué)習(xí)更多的關(guān)于類或者協(xié)議,我們需要看 API 文檔。蘋果提供了簡單的方法來訪問在 Xcode 里的文檔。所有你需要做的是把光標放置在類或者協(xié)議上(如 UITableViewController)然后按住’control-command-?’。這將打開一個彈出類的細節(jié)比如它已經(jīng)添加的協(xié)議。

點擊 UITableViewDelegate 將進一步打開一個文檔瀏覽器。從那里,你會發(fā)現(xiàn)協(xié)議中定義的所有辦法。

通過粗略的看文檔,你會發(fā)現(xiàn)下面的方法來管理行選擇:
- tableView(_:willSelectRowAtIndexPath:)
- tableView(_:didSelectRowAtIndexPath:)
這兩個方法都是為行選擇設(shè)計的。唯一的不同是,當指定行被選擇的時候,會調(diào)用tableView(:willSelectRowAtIndexPath:)。你可以使用這個方法來放置特定的單元格。你使用 tableView(:didSelectRowAtIndexPath:) 方法。在用戶選擇一行后調(diào)用這個方法來跟進行選擇。我們將在選擇行以后實現(xiàn)這個方法來執(zhí)行額外添加的任務(wù)(如彈出一個菜單)。
通過實現(xiàn)協(xié)議管理行選擇(Row Selections)
Okey,解釋得足夠多了。讓我們來到有趣的部分然后寫一些代碼。在 FoodPin 工程里,打開 RestaurantTableViewController.swift 文件然后在 RestaurantTableViewController 類里實現(xiàn)tableView(_:didselectRowAtIndexPath:) 方法:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//創(chuàng)建一個選項菜單作為動作表單
let optionMenu = UIAlertController(title: nil, message: “what do you want to do?”, preferredStyle: .ActionSheet)
//添加動作到菜單
let cancelAction = UIAlertAction(title: “Cancel”, style: .Cancel, handler: nil)
optionMenu.addAction(cancelAction)
//顯示菜單
self.presentViewController(optionMenu, animated: true, completion: nil)
}
上面的代碼通過實例化一個 UIAlertController 對象來創(chuàng)建一個選項菜單。當用戶點擊table view 里的任何行,這個方法將自動被調(diào)用來彈出動作表單顯示“你想要做什么”的信息和一個取消按鈕。嘗試運行工程來進行快速的測試。app 應(yīng)該可以檢測到觸摸。

更多的關(guān)于 UIAlertController
在我們繼續(xù)之前,讓我們討論下更多關(guān)于 UIAlertController 類。UIAlertController 類在 iOS8里第一次被介紹來替換舊版本的 iOS SDK 里的 UIAlertview和 UIActionSheet 類。它是為顯示警告信息給用戶而設(shè)計的。
提及前面章節(jié)的代碼片段,你可以通過 preferredStyle 參數(shù)來制定 UIAlertController 對象的樣式。你也可以設(shè)置它自己的值到.ActionSheet或者.Alert。下圖顯示警告樣式的例子。

除了顯示信息給用戶以外,你也可以行動給警報控制器來給用戶一個回應(yīng)的方法。要做到這點,你應(yīng)該創(chuàng)建一個 UIAlertAction 對象,這個對象有著你首選的標題,樣式和代碼塊來執(zhí)行行動。在代碼片段里,我們創(chuàng)建一個 標題為’Cancel’,樣式為’.Cancel’的cancelAction 類。當用戶選擇取消動作的時候不會有任何執(zhí)行。因此,處理程序被設(shè)置成空值(nil)。在 UIAlertAction 對象創(chuàng)建后,你可以通過使用 addAction 方法來非配它給警告控制器。
當警告控制器正確配置的時候,你可以用 presentViewController 方法簡單的介紹它。
這是你如何用 UIAlertController 類來介紹一個警告。作為一個初學(xué)者,你可能有一些問題:
- 我如何知道當創(chuàng)建一個 UIAlertController 對象時 preferredStyle 參數(shù)值是可用的?
- 點(.)語法看起來很新鮮。它應(yīng)該寫成 UIAlertControllerStyle.ActionsSheet 嗎?
這些都是好問題。
第一個問題,再說一次答案是“參考文檔”。在 Xcode 里,你可以把指針放到 preferredStyle 參數(shù)上然后按 control-command-?。Xcode 將顯示方法聲明。你可以進一步點擊 UIAlertControllerStyle 來閱讀 API 參考文檔。就像你下圖看到的,UIAlertControllerStye 是一個枚舉,它定義了兩個可能的值:ActionSheet 和 Alert。

Quick note:枚舉在 Swift 里是一個常見的格式,它為這種格式定義了一列可能的值。UIAlertControllerStyle 是一個好例子。
我們可以用 UIAlertControllerStyle.ActionSheet 或者 UIAlertControllerStyle.Alert 來查閱值。所以當你創(chuàng)建一個 UIAlertController 時你可以寫像這樣的代碼:
let optionMenu = UIAlertController(title: nil, message: “What do you want to do?”), preferredStyle: UIAlertControllerStyle.ActionSheet)
上面的代碼沒有一點錯。Swift 給開發(fā)者一個速記的辦法,幫助我們打更少的代碼。因為 preferredStyle 參數(shù)的格式已經(jīng)知道(如 UIAlertControllerStyle),Swift 讓你用更短的.語法來省略 UIAlertControllerStyle。這就是為什么我們像這樣實例化 UIAlertController 對象:
let optionMenu = UIAlertController(title: nil, message: “What do you want to do?”, preferredStyle: .ActionSheet)
這同樣適用于 UIAlertActionStyle。UIAlertActionStyle 是一個有著3個可能值的枚舉:Default,Cancel和Destructive。當創(chuàng)建cancelAction對象時,我們同樣使用簡寫語法:
let cancelAction = UIAlertAction(title: “Cancel”, style: .Cancel, handler: nil)
添加動作到警告控制器
現(xiàn)在讓我們添加兩個更多的動作到警告控制器:
- “Call”動作-打電話給被選擇的餐廳。我們將填入一個偽造的電話號碼顯示“Call 123-000-x”。
- “I’ve been here” 動作 - 當被選擇的適合,這個選項添加一個復(fù)選框給被選擇的餐廳。
在 tableView(_:didSelectRowAtIndexPath:) 方法里,為“Call”添加下面的代碼。你可以在 cancelAction 的初始值之后插入代碼:
let callActionHandler = {(action:UIAlertAction!) -> Void in
let alertMessage = UIAlertController(title: “Service Unavailable”, message: “sorry, the call feature is not availabel yet. Please retry later.”, preferredStyle: .Alert)
alertMessage.addAction(UIAlertAction(title: “OK”, style: .Default, handler:nil))
self.presentViewController(alertMessage, animated: true, completion: nil)
}
let callAction = UIAlertAction(title: “call” + “123-000-(indexPath.row)",style: UIAlertActionStyle.Default, handler: callActionHandler)
optionMenu.addAction(callAction)
在上面的代碼里,你可能對 callActionHandler 對象不熟悉。像之前提到的,你可以在創(chuàng)建一個 UIAlertAction 對象的時候制定一個代碼塊作為處理程序。當用戶選擇行動的時候?qū)?zhí)行這個代碼塊。那意味著我們對于 取消按鈕沒有任何后續(xù)行動。
對于 callAction 對象,我們用 callActionHandler 來分配它。代碼塊顯示一個警告,告訴用戶打電話的特征還不可用。
在 Swift 里,這個代碼塊被稱作閉包(Closure)。Closure是獨立的方法塊,它可以在你的代碼里傳遞。這和 Objective-C 里的塊(blocks)非常相似。像上面的例子,提供行動閉包的一個方法是用代碼塊的值作為常量或變量來聲明它。代碼塊的第一部分對于處理程序參數(shù)的定義是一樣的。in 關(guān)鍵詞表示閉包定義的參數(shù)和返回類型已經(jīng)完成,閉包的主體將開始。下圖說明了一個閉包的語法。

callAction 對象的標題是一個假設(shè)的電話號碼。它是由選中的索引行連接’123-000-‘生成的。如你所見在代碼里,Swift 允許開發(fā)者用加號(+)來聯(lián)系字符串。所有你需要的是用括號括起來,前面加反斜杠():
“Call” + “123-000-(indexPath.row)”
Quick note:在 Playgrounds 章已經(jīng)介紹過字符串的串聯(lián)。如果你翻到第二章的聯(lián)系,是時候再次訪問它了。或者,你可以參考附件。
隨著 Call 動作的實現(xiàn),為”I’ve been here"動作添加下面的代碼行:
let isVisitedAction = UIAlertAction(title: “I’ve been here”, style: .Default, handler: {
(action:UIAlertAction) ->Void in
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.accessoryType = .Checkmark
})
optionMenu.addAction(isVisitedAction)
上面的方法給你展示了另一種方法來使用閉包。你可以寫一個內(nèi)聯(lián)的閉包作為處理程序的參數(shù)。這是讓代碼更清晰,更可讀的首選方法。
Swift 里的可選項
你可能想知道問號是做什么用的。單元格在 Swift 里被認為是一個可選項。在 Swift 里介紹了一個新的格式叫做可選項(Optional)??蛇x項簡單的意思是“這里有一個值”或者“這里根本沒有值”。單元格通過 tableView 返回。cellForRowAtIndexPath 是一個可選項。用問號來訪問 accessoryType 單元格的特性。在這種情況下,Swift 將檢查單元格是否存在,如果單元格存在,允許你來設(shè)置 accessoryType 的值。在大多數(shù)情況下,當你訪問一個可選的屬性時,Xcode 的自動補全特性會為你添加問號。為了學(xué)習(xí)更多的關(guān)于可選項,你可以進一步的參考附錄。
當一個用戶選擇”I’ve been here”選項,我們添加給選中的單元格添加一個復(fù)選框。在 table view 單元格里,右邊部分是留給輔助視圖的。有4種類型的內(nèi)置輔助視圖包括展開指示器 (disclosure indicator),詳情展開按鈕(detail disclosure button),復(fù)選框(checkmark)和細節(jié)(detail)。在這種情況下,我們使用 checkmark 作為指示器。
代碼塊的第一行使用 indexPath 檢索所選的單元格,它包括了所選單元格的索引。第二行代碼用一個復(fù)選標記更新了 accessoryType 單元格的性質(zhì)。
編譯運行 app。按一個餐廳然后選擇其中一個行為,它將展現(xiàn)一個復(fù)選標記或者警告給你。

現(xiàn)在,當你選擇一行,高亮顯示灰色和保持選中的行。在 tableView(_:didSelectRowAtIndexPath:) 方法的最后添加下面的代碼來取消選定的行。
tableView.deselectRowAtIndexPath(indexPath, animated: false)
我們遇到了 Bug
app 看起來很好。但是如果你近距離觀察它,app里有一個 bug。你用’I’ve been here'標記了’Cafe Deadend’餐廳。如果你往下滾,你會找到另一個餐廳(如Palomino Espresso)同樣包含一個復(fù)選框。發(fā)生了什么問題?為什么 app 會添加額外的復(fù)選框?
像每個程序員一樣,我討厭 bug 尤其是當面臨一個工程快要交貨的時候。但是 bug 總是能幫助我提高我的編程技巧。如果你繼續(xù)學(xué)習(xí)你也會遇到很多 bug。習(xí)慣它吧。
出現(xiàn)這個問題是由于單元格被重復(fù)使用,這個我們在之前的章節(jié)已經(jīng)討論過了。例如,table view 有30個單元格。由于性能原因,UITableView 可能只創(chuàng)造了10個單元格,當你滾動表的時候來重復(fù)使用他們,來代替創(chuàng)造30個單元格。這種情況下,UITableView 重復(fù)使用第一個單元格(最初當做一個復(fù)選框用于Cafe Deadend)來顯示另一個餐廳。在我們的代碼里,當 table view 重復(fù)使用同樣的單元格時,我們僅僅更新了圖片視圖和標簽。附屬視圖并沒有更新。因此,下一個餐廳重復(fù)使用同樣的單元格共用同樣的附屬視圖。如果附屬視圖包含一個復(fù)選框,那個餐廳同樣帶著一個復(fù)選框。
我們?nèi)绾谓鉀Q這個 bug?
我們必須找到另一種方式來跟蹤檢查項。創(chuàng)造另一個數(shù)組來保存被檢查的餐廳怎么樣?在 RestaurantTableViewController.swift 文件里,聲明一個 Boolean 數(shù)組:
var restaurantIsVisited = [Bool](count: 21, repeatedValue: false)
Swift 里Bool是一個數(shù)據(jù)類型,擁有一個 布林(Boolean)值。Swift 提供2個布林(Boolean)值:true 和 false。我們聲明restaurantIsVisited數(shù)組來保留一個 Bool 值的合集。每一個數(shù)組中的值顯示是否對應(yīng)的餐廳被標記為”I’ve been here”。例如,我們可以觀察 restaurantIsVisited[0]的值來Cafe Deadend 是否已經(jīng)被檢查或者沒有。
數(shù)組里的值被初始化為 false。換句話說,條目默認是沒有檢查的。上面的代碼行用重復(fù)的值顯示一個方法來初始化一個數(shù)組在 Swift 里。初始值如下:
var restaurantIsVisited = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false]
我們必須做一些改變來修復(fù) bug。首先,當一個餐廳被檢查時我們需要更新 Bool 數(shù)組的值。在 isVisitedAction 對象的處理程序中添加一行代碼:
let isVisitedAction = UIAlertAction(title: “I’ve been here”, style: .Default, handler: {
(action:UIAlertAction!) -> Void in
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.accessoryType = .Checkmark
self.restaurantIsVisited[indexPath.row] = true
})
代碼非常直白。我們把被選的值從false變成 true。最后,在return cell之前添加一些代碼行來更新在 tableView(_:cellForRowAtIndexPath:) 方法附屬視圖:
if restaurantIsVisited[indexPath.row] {
cell.accessoryType = .Checkmark
} else {
cell.accessoryType = .None
}
現(xiàn)在,再次編譯運行 app?,F(xiàn)在你的 bug 應(yīng)該解決了。
你可以進一步使用三元條件運算符來把上面的 if 條件簡化成一行代碼(?:):
cell.accessoryType = restaurantIsVisited[indexPath.row] ? .Checkmark : .None
三元條件運算符是為評估簡單的條件做的一個高效的速寫。