第十章 與 table view 互動和使用 UIAlertController

QQ20151214-6@2x.png

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 里顯示警告。

QQ20151214-0@2x.png

理解 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-?’。這將打開一個彈出類的細節比如它已經添加的協議。

QQ20151214-1@2x.png

點擊 UITableViewDelegate 將進一步打開一個文檔瀏覽器。從那里,你會發現協議中定義的所有辦法。
QQ20151214-2@2x.png

通過粗略的看文檔,你會發現下面的方法來管理行選擇:

  • 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 應該可以檢測到觸摸。


QQ20151214-3@2x.png

更多的關于 UIAlertController

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

QQ20151214-4@2x.png

除了顯示信息給用戶以外,你也可以行動給警報控制器來給用戶一個回應的方法。要做到這點,你應該創建一個 UIAlertAction 對象,這個對象有著你首選的標題,樣式和代碼塊來執行行動。在代碼片段里,我們創建一個 標題為’Cancel’,樣式為’.Cancel’的cancelAction 類。當用戶選擇取消動作的時候不會有任何執行。因此,處理程序被設置成空值(nil)。在 UIAlertAction 對象創建后,你可以通過使用 addAction 方法來非配它給警告控制器。
當警告控制器正確配置的時候,你可以用 presentViewController 方法簡單的介紹它。
這是你如何用 UIAlertController 類來介紹一個警告。作為一個初學者,你可能有一些問題:

  • 我如何知道當創建一個 UIAlertController 對象時 preferredStyle 參數值是可用的?
  • 點(.)語法看起來很新鮮。它應該寫成 UIAlertControllerStyle.ActionsSheet 嗎?

這些都是好問題。
第一個問題,再說一次答案是“參考文檔”。在 Xcode 里,你可以把指針放到 preferredStyle 參數上然后按 control-command-?。Xcode 將顯示方法聲明。你可以進一步點擊 UIAlertControllerStyle 來閱讀 API 參考文檔。就像你下圖看到的,UIAlertControllerStye 是一個枚舉,它定義了兩個可能的值:ActionSheet 和 Alert。


QQ20151215-0@2x.png

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

三元條件運算符是為評估簡單的條件做的一個高效的速寫。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容