There is no learning without trying lots of ideas and failing lots of times.
- Jonathan Ive
到目前為止,我們僅僅專注于在一個 table view 里顯示數據。我猜你應該在想我們怎樣才能與 table view 互動和檢測行選擇的。這是我們這章將要討論的。
我們將繼續改進我們在之前章節構建的 FoodPin app,添加一些增強功能:
- 當用戶按單元格的時候彈出一個菜單。這個菜單提供兩個選項:Call和I’ve been here。
- 當用戶選擇“I’ve been here”時顯示一個心形的圖標。
通過實施這些新功能,你還將學習如何使用 UIAlertController,它通常用于在 iOS apps 里顯示警告。
理解 UITableViewDelegate 協議
當我們在第八章首次構建 SimpleTable app,我們添加了2個委托:UITableViewDelegate 和 UITableViewDataSource,到 RestaurantTableViewController 類里。我已經和你們討論過 UITableViewDataSource 協議但是僅僅提到了 UITableViewDelegate 協議。
像之前說的,委托模式在 iOS 編程里是非常常見的。每個委托負責一個特定角色或者任務來保持系統簡單和干凈。當一個對象需要執行特定的任務時,它依賴于另一個對象來處理它。這在軟件設計里通常被稱作“關注點分離(separation of concerns)”。
UITableView 類提供這個設計概念。這兩個協議為了不同的目的而設計。UITableViewDataSource 協議定義方法,被用來管理表格數據。它依賴于委托(delegate)所提供的表格數據(table data)。另一方面,UITableViewDelegate 協議負責設置table view的頁眉和頁腳部分,同時也處理單元格選擇和單元格重新排序。
為了管理行選擇(row selection),我們將在 UITableViewDelegate 協議里執行一些方法。
閱讀文檔
在執行方法之前,你可能想知道:
我們如何知道 UITableViewDelegate 里哪個方法將被執行呢?
答案就是“閱讀文檔”。你已經獲得了免費訪問蘋果官方 iOS 開發者文檔(https://developer.apple.com/library/ios/)的權利。作為 iOS 開發者,你需要適應閱讀 API 文檔。地球上沒有一本書能覆蓋關于 iOS SDK 的所有事情。大多數時間當我們想要學習更多的關于類或者協議,我們需要看 API 文檔。蘋果提供了簡單的方法來訪問在 Xcode 里的文檔。所有你需要做的是把光標放置在類或者協議上(如 UITableViewController)然后按住’control-command-?’。這將打開一個彈出類的細節比如它已經添加的協議。
點擊 UITableViewDelegate 將進一步打開一個文檔瀏覽器。從那里,你會發現協議中定義的所有辦法。
通過粗略的看文檔,你會發現下面的方法來管理行選擇:
- tableView(_:willSelectRowAtIndexPath:)
- tableView(_:didSelectRowAtIndexPath:)
這兩個方法都是為行選擇設計的。唯一的不同是,當指定行被選擇的時候,會調用tableView(:willSelectRowAtIndexPath:)。你可以使用這個方法來放置特定的單元格。你使用 tableView(:didSelectRowAtIndexPath:) 方法。在用戶選擇一行后調用這個方法來跟進行選擇。我們將在選擇行以后實現這個方法來執行額外添加的任務(如彈出一個菜單)。
通過實現協議管理行選擇(Row Selections)
Okey,解釋得足夠多了。讓我們來到有趣的部分然后寫一些代碼。在 FoodPin 工程里,打開 RestaurantTableViewController.swift 文件然后在 RestaurantTableViewController 類里實現tableView(_:didselectRowAtIndexPath:) 方法:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//創建一個選項菜單作為動作表單
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 對象來創建一個選項菜單。當用戶點擊table view 里的任何行,這個方法將自動被調用來彈出動作表單顯示“你想要做什么”的信息和一個取消按鈕。嘗試運行工程來進行快速的測試。app 應該可以檢測到觸摸。
更多的關于 UIAlertController
在我們繼續之前,讓我們討論下更多關于 UIAlertController 類。UIAlertController 類在 iOS8里第一次被介紹來替換舊版本的 iOS SDK 里的 UIAlertview和 UIActionSheet 類。它是為顯示警告信息給用戶而設計的。
提及前面章節的代碼片段,你可以通過 preferredStyle 參數來制定 UIAlertController 對象的樣式。你也可以設置它自己的值到.ActionSheet或者.Alert。下圖顯示警告樣式的例子。
除了顯示信息給用戶以外,你也可以行動給警報控制器來給用戶一個回應的方法。要做到這點,你應該創建一個 UIAlertAction 對象,這個對象有著你首選的標題,樣式和代碼塊來執行行動。在代碼片段里,我們創建一個 標題為’Cancel’,樣式為’.Cancel’的cancelAction 類。當用戶選擇取消動作的時候不會有任何執行。因此,處理程序被設置成空值(nil)。在 UIAlertAction 對象創建后,你可以通過使用 addAction 方法來非配它給警告控制器。
當警告控制器正確配置的時候,你可以用 presentViewController 方法簡單的介紹它。
這是你如何用 UIAlertController 類來介紹一個警告。作為一個初學者,你可能有一些問題:
- 我如何知道當創建一個 UIAlertController 對象時 preferredStyle 參數值是可用的?
- 點(.)語法看起來很新鮮。它應該寫成 UIAlertControllerStyle.ActionsSheet 嗎?
這些都是好問題。
第一個問題,再說一次答案是“參考文檔”。在 Xcode 里,你可以把指針放到 preferredStyle 參數上然后按 control-command-?。Xcode 將顯示方法聲明。你可以進一步點擊 UIAlertControllerStyle 來閱讀 API 參考文檔。就像你下圖看到的,UIAlertControllerStye 是一個枚舉,它定義了兩個可能的值:ActionSheet 和 Alert。
Quick note:枚舉在 Swift 里是一個常見的格式,它為這種格式定義了一列可能的值。UIAlertControllerStyle 是一個好例子。
我們可以用 UIAlertControllerStyle.ActionSheet 或者 UIAlertControllerStyle.Alert 來查閱值。所以當你創建一個 UIAlertController 時你可以寫像這樣的代碼:
let optionMenu = UIAlertController(title: nil, message: “What do you want to do?”), preferredStyle: UIAlertControllerStyle.ActionSheet)
上面的代碼沒有一點錯。Swift 給開發者一個速記的辦法,幫助我們打更少的代碼。因為 preferredStyle 參數的格式已經知道(如 UIAlertControllerStyle),Swift 讓你用更短的.語法來省略 UIAlertControllerStyle。這就是為什么我們像這樣實例化 UIAlertController 對象:
let optionMenu = UIAlertController(title: nil, message: “What do you want to do?”, preferredStyle: .ActionSheet)
這同樣適用于 UIAlertActionStyle。UIAlertActionStyle 是一個有著3個可能值的枚舉:Default,Cancel和Destructive。當創建cancelAction對象時,我們同樣使用簡寫語法:
let cancelAction = UIAlertAction(title: “Cancel”, style: .Cancel, handler: nil)
添加動作到警告控制器
現在讓我們添加兩個更多的動作到警告控制器:
- “Call”動作-打電話給被選擇的餐廳。我們將填入一個偽造的電話號碼顯示“Call 123-000-x”。
- “I’ve been here” 動作 - 當被選擇的適合,這個選項添加一個復選框給被選擇的餐廳。
在 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 對象不熟悉。像之前提到的,你可以在創建一個 UIAlertAction 對象的時候制定一個代碼塊作為處理程序。當用戶選擇行動的時候將執行這個代碼塊。那意味著我們對于 取消按鈕沒有任何后續行動。
對于 callAction 對象,我們用 callActionHandler 來分配它。代碼塊顯示一個警告,告訴用戶打電話的特征還不可用。
在 Swift 里,這個代碼塊被稱作閉包(Closure)。Closure是獨立的方法塊,它可以在你的代碼里傳遞。這和 Objective-C 里的塊(blocks)非常相似。像上面的例子,提供行動閉包的一個方法是用代碼塊的值作為常量或變量來聲明它。代碼塊的第一部分對于處理程序參數的定義是一樣的。in 關鍵詞表示閉包定義的參數和返回類型已經完成,閉包的主體將開始。下圖說明了一個閉包的語法。
callAction 對象的標題是一個假設的電話號碼。它是由選中的索引行連接’123-000-‘生成的。如你所見在代碼里,Swift 允許開發者用加號(+)來聯系字符串。所有你需要的是用括號括起來,前面加反斜杠():
“Call” + “123-000-(indexPath.row)”
Quick note:在 Playgrounds 章已經介紹過字符串的串聯。如果你翻到第二章的聯系,是時候再次訪問它了。或者,你可以參考附件。
隨著 Call 動作的實現,為”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)
上面的方法給你展示了另一種方法來使用閉包。你可以寫一個內聯的閉包作為處理程序的參數。這是讓代碼更清晰,更可讀的首選方法。
Swift 里的可選項
你可能想知道問號是做什么用的。單元格在 Swift 里被認為是一個可選項。在 Swift 里介紹了一個新的格式叫做可選項(Optional)。可選項簡單的意思是“這里有一個值”或者“這里根本沒有值”。單元格通過 tableView 返回。cellForRowAtIndexPath 是一個可選項。用問號來訪問 accessoryType 單元格的特性。在這種情況下,Swift 將檢查單元格是否存在,如果單元格存在,允許你來設置 accessoryType 的值。在大多數情況下,當你訪問一個可選的屬性時,Xcode 的自動補全特性會為你添加問號。為了學習更多的關于可選項,你可以進一步的參考附錄。
當一個用戶選擇”I’ve been here”選項,我們添加給選中的單元格添加一個復選框。在 table view 單元格里,右邊部分是留給輔助視圖的。有4種類型的內置輔助視圖包括展開指示器 (disclosure indicator),詳情展開按鈕(detail disclosure button),復選框(checkmark)和細節(detail)。在這種情況下,我們使用 checkmark 作為指示器。
代碼塊的第一行使用 indexPath 檢索所選的單元格,它包括了所選單元格的索引。第二行代碼用一個復選標記更新了 accessoryType 單元格的性質。
編譯運行 app。按一個餐廳然后選擇其中一個行為,它將展現一個復選標記或者警告給你。
現在,當你選擇一行,高亮顯示灰色和保持選中的行。在 tableView(_:didSelectRowAtIndexPath:) 方法的最后添加下面的代碼來取消選定的行。
tableView.deselectRowAtIndexPath(indexPath, animated: false)
我們遇到了 Bug
app 看起來很好。但是如果你近距離觀察它,app里有一個 bug。你用’I’ve been here'標記了’Cafe Deadend’餐廳。如果你往下滾,你會找到另一個餐廳(如Palomino Espresso)同樣包含一個復選框。發生了什么問題?為什么 app 會添加額外的復選框?
像每個程序員一樣,我討厭 bug 尤其是當面臨一個工程快要交貨的時候。但是 bug 總是能幫助我提高我的編程技巧。如果你繼續學習你也會遇到很多 bug。習慣它吧。
出現這個問題是由于單元格被重復使用,這個我們在之前的章節已經討論過了。例如,table view 有30個單元格。由于性能原因,UITableView 可能只創造了10個單元格,當你滾動表的時候來重復使用他們,來代替創造30個單元格。這種情況下,UITableView 重復使用第一個單元格(最初當做一個復選框用于Cafe Deadend)來顯示另一個餐廳。在我們的代碼里,當 table view 重復使用同樣的單元格時,我們僅僅更新了圖片視圖和標簽。附屬視圖并沒有更新。因此,下一個餐廳重復使用同樣的單元格共用同樣的附屬視圖。如果附屬視圖包含一個復選框,那個餐廳同樣帶著一個復選框。
我們如何解決這個 bug?
我們必須找到另一種方式來跟蹤檢查項。創造另一個數組來保存被檢查的餐廳怎么樣?在 RestaurantTableViewController.swift 文件里,聲明一個 Boolean 數組:
var restaurantIsVisited = [Bool](count: 21, repeatedValue: false)
Swift 里Bool是一個數據類型,擁有一個 布林(Boolean)值。Swift 提供2個布林(Boolean)值:true 和 false。我們聲明restaurantIsVisited數組來保留一個 Bool 值的合集。每一個數組中的值顯示是否對應的餐廳被標記為”I’ve been here”。例如,我們可以觀察 restaurantIsVisited[0]的值來Cafe Deadend 是否已經被檢查或者沒有。
數組里的值被初始化為 false。換句話說,條目默認是沒有檢查的。上面的代碼行用重復的值顯示一個方法來初始化一個數組在 Swift 里。初始值如下:
var restaurantIsVisited = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false]
我們必須做一些改變來修復 bug。首先,當一個餐廳被檢查時我們需要更新 Bool 數組的值。在 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
}
現在,再次編譯運行 app。現在你的 bug 應該解決了。
你可以進一步使用三元條件運算符來把上面的 if 條件簡化成一行代碼(?:):
cell.accessoryType = restaurantIsVisited[indexPath.row] ? .Checkmark : .None
三元條件運算符是為評估簡單的條件做的一個高效的速寫。