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ù)改進(jìn)我們在之前章節(jié)構(gòu)建的 FoodPin app,添加一些增強(qiáng)功能:
- 當(dāng)用戶按單元格的時候彈出一個菜單皂甘。這個菜單提供兩個選項(xiàng):Call和I’ve been here鲜滩。
- 當(dāng)用戶選擇“I’ve been here”時顯示一個心形的圖標(biāo)。
通過實(shí)施這些新功能婴削,你還將學(xué)習(xí)如何使用 UIAlertController,它通常用于在 iOS apps 里顯示警告牙肝。
理解 UITableViewDelegate 協(xié)議
當(dāng)我們在第八章首次構(gòu)建 SimpleTable app唉俗,我們添加了2個委托:UITableViewDelegate 和 UITableViewDataSource,到 RestaurantTableViewController 類里配椭。我已經(jīng)和你們討論過 UITableViewDataSource 協(xié)議但是僅僅提到了 UITableViewDelegate 協(xié)議虫溜。
像之前說的,委托模式在 iOS 編程里是非常常見的股缸。每個委托負(fù)責(zé)一個特定角色或者任務(wù)來保持系統(tǒng)簡單和干凈衡楞。當(dāng)一個對象需要執(zhí)行特定的任務(wù)時,它依賴于另一個對象來處理它敦姻。這在軟件設(shè)計(jì)里通常被稱作“關(guān)注點(diǎn)分離(separation of concerns)”瘾境。
UITableView 類提供這個設(shè)計(jì)概念。這兩個協(xié)議為了不同的目的而設(shè)計(jì)镰惦。UITableViewDataSource 協(xié)議定義方法迷守,被用來管理表格數(shù)據(jù)。它依賴于委托(delegate)所提供的表格數(shù)據(jù)(table data)旺入。另一方面兑凿,UITableViewDelegate 協(xié)議負(fù)責(zé)設(shè)置table view的頁眉和頁腳部分凯力,同時也處理單元格選擇和單元格重新排序。
為了管理行選擇(row selection)礼华,我們將在 UITableViewDelegate 協(xié)議里執(zhí)行一些方法沮协。
閱讀文檔
在執(zhí)行方法之前,你可能想知道:
我們?nèi)绾沃?UITableViewDelegate 里哪個方法將被執(zhí)行呢卓嫂?
答案就是“閱讀文檔”慷暂。你已經(jīng)獲得了免費(fèi)訪問蘋果官方 iOS 開發(fā)者文檔(https://developer.apple.com/library/ios/)的權(quán)利。作為 iOS 開發(fā)者晨雳,你需要適應(yīng)閱讀 API 文檔行瑞。地球上沒有一本書能覆蓋關(guān)于 iOS SDK 的所有事情。大多數(shù)時間當(dāng)我們想要學(xué)習(xí)更多的關(guān)于類或者協(xié)議餐禁,我們需要看 API 文檔血久。蘋果提供了簡單的方法來訪問在 Xcode 里的文檔。所有你需要做的是把光標(biāo)放置在類或者協(xié)議上(如 UITableViewController)然后按住’control-command-?’帮非。這將打開一個彈出類的細(xì)節(jié)比如它已經(jīng)添加的協(xié)議氧吐。
點(diǎn)擊 UITableViewDelegate 將進(jìn)一步打開一個文檔瀏覽器。從那里末盔,你會發(fā)現(xiàn)協(xié)議中定義的所有辦法筑舅。
通過粗略的看文檔,你會發(fā)現(xiàn)下面的方法來管理行選擇:
- tableView(_:willSelectRowAtIndexPath:)
- tableView(_:didSelectRowAtIndexPath:)
這兩個方法都是為行選擇設(shè)計(jì)的陨舱。唯一的不同是翠拣,當(dāng)指定行被選擇的時候,會調(diào)用tableView(:willSelectRowAtIndexPath:)游盲。你可以使用這個方法來放置特定的單元格误墓。你使用 tableView(:didSelectRowAtIndexPath:) 方法。在用戶選擇一行后調(diào)用這個方法來跟進(jìn)行選擇益缎。我們將在選擇行以后實(shí)現(xiàn)這個方法來執(zhí)行額外添加的任務(wù)(如彈出一個菜單)谜慌。
通過實(shí)現(xiàn)協(xié)議管理行選擇(Row Selections)
Okey,解釋得足夠多了莺奔。讓我們來到有趣的部分然后寫一些代碼欣范。在 FoodPin 工程里,打開 RestaurantTableViewController.swift 文件然后在 RestaurantTableViewController 類里實(shí)現(xiàn)tableView(_:didselectRowAtIndexPath:) 方法:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//創(chuàng)建一個選項(xià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)
}
上面的代碼通過實(shí)例化一個 UIAlertController 對象來創(chuàng)建一個選項(xiàng)菜單弊仪。當(dāng)用戶點(diǎn)擊table view 里的任何行熙卡,這個方法將自動被調(diào)用來彈出動作表單顯示“你想要做什么”的信息和一個取消按鈕。嘗試運(yùn)行工程來進(jìn)行快速的測試励饵。app 應(yīng)該可以檢測到觸摸。
更多的關(guān)于 UIAlertController
在我們繼續(xù)之前滑燃,讓我們討論下更多關(guān)于 UIAlertController 類役听。UIAlertController 類在 iOS8里第一次被介紹來替換舊版本的 iOS SDK 里的 UIAlertview和 UIActionSheet 類。它是為顯示警告信息給用戶而設(shè)計(jì)的。
提及前面章節(jié)的代碼片段典予,你可以通過 preferredStyle 參數(shù)來制定 UIAlertController 對象的樣式甜滨。你也可以設(shè)置它自己的值到.ActionSheet或者.Alert。下圖顯示警告樣式的例子瘤袖。
除了顯示信息給用戶以外衣摩,你也可以行動給警報(bào)控制器來給用戶一個回應(yīng)的方法。要做到這點(diǎn)捂敌,你應(yīng)該創(chuàng)建一個 UIAlertAction 對象艾扮,這個對象有著你首選的標(biāo)題,樣式和代碼塊來執(zhí)行行動占婉。在代碼片段里泡嘴,我們創(chuàng)建一個 標(biāo)題為’Cancel’,樣式為’.Cancel’的cancelAction 類逆济。當(dāng)用戶選擇取消動作的時候不會有任何執(zhí)行酌予。因此,處理程序被設(shè)置成空值(nil)奖慌。在 UIAlertAction 對象創(chuàng)建后抛虫,你可以通過使用 addAction 方法來非配它給警告控制器。
當(dāng)警告控制器正確配置的時候简僧,你可以用 presentViewController 方法簡單的介紹它莱褒。
這是你如何用 UIAlertController 類來介紹一個警告。作為一個初學(xué)者涎劈,你可能有一些問題:
- 我如何知道當(dāng)創(chuàng)建一個 UIAlertController 對象時 preferredStyle 參數(shù)值是可用的广凸?
- 點(diǎn)(.)語法看起來很新鮮。它應(yīng)該寫成 UIAlertControllerStyle.ActionsSheet 嗎蛛枚?
這些都是好問題谅海。
第一個問題,再說一次答案是“參考文檔”蹦浦。在 Xcode 里扭吁,你可以把指針放到 preferredStyle 參數(shù)上然后按 control-command-?。Xcode 將顯示方法聲明盲镶。你可以進(jìn)一步點(diǎn)擊 UIAlertControllerStyle 來閱讀 API 參考文檔侥袜。就像你下圖看到的,UIAlertControllerStye 是一個枚舉溉贿,它定義了兩個可能的值:ActionSheet 和 Alert枫吧。
Quick note:枚舉在 Swift 里是一個常見的格式,它為這種格式定義了一列可能的值宇色。UIAlertControllerStyle 是一個好例子九杂。
我們可以用 UIAlertControllerStyle.ActionSheet 或者 UIAlertControllerStyle.Alert 來查閱值颁湖。所以當(dāng)你創(chuàng)建一個 UIAlertController 時你可以寫像這樣的代碼:
let optionMenu = UIAlertController(title: nil, message: “What do you want to do?”), preferredStyle: UIAlertControllerStyle.ActionSheet)
上面的代碼沒有一點(diǎn)錯。Swift 給開發(fā)者一個速記的辦法例隆,幫助我們打更少的代碼甥捺。因?yàn)?preferredStyle 參數(shù)的格式已經(jīng)知道(如 UIAlertControllerStyle),Swift 讓你用更短的.語法來省略 UIAlertControllerStyle镀层。這就是為什么我們像這樣實(shí)例化 UIAlertController 對象:
let optionMenu = UIAlertController(title: nil, message: “What do you want to do?”, preferredStyle: .ActionSheet)
這同樣適用于 UIAlertActionStyle镰禾。UIAlertActionStyle 是一個有著3個可能值的枚舉:Default,Cancel和Destructive。當(dāng)創(chuàng)建cancelAction對象時唱逢,我們同樣使用簡寫語法:
let cancelAction = UIAlertAction(title: “Cancel”, style: .Cancel, handler: nil)
添加動作到警告控制器
現(xiàn)在讓我們添加兩個更多的動作到警告控制器:
- “Call”動作-打電話給被選擇的餐廳吴侦。我們將填入一個偽造的電話號碼顯示“Call 123-000-x”。
- “I’ve been here” 動作 - 當(dāng)被選擇的適合惶我,這個選項(xiàng)添加一個復(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 對象的時候制定一個代碼塊作為處理程序捧挺。當(dāng)用戶選擇行動的時候?qū)?zhí)行這個代碼塊。那意味著我們對于 取消按鈕沒有任何后續(xù)行動尿瞭。
對于 callAction 對象闽烙,我們用 callActionHandler 來分配它。代碼塊顯示一個警告声搁,告訴用戶打電話的特征還不可用黑竞。
在 Swift 里,這個代碼塊被稱作閉包(Closure)疏旨。Closure是獨(dú)立的方法塊很魂,它可以在你的代碼里傳遞。這和 Objective-C 里的塊(blocks)非常相似檐涝。像上面的例子遏匆,提供行動閉包的一個方法是用代碼塊的值作為常量或變量來聲明它。代碼塊的第一部分對于處理程序參數(shù)的定義是一樣的谁榜。in 關(guān)鍵詞表示閉包定義的參數(shù)和返回類型已經(jīng)完成幅聘,閉包的主體將開始。下圖說明了一個閉包的語法窃植。
callAction 對象的標(biāo)題是一個假設(shè)的電話號碼帝蒿。它是由選中的索引行連接’123-000-‘生成的。如你所見在代碼里撕瞧,Swift 允許開發(fā)者用加號(+)來聯(lián)系字符串。所有你需要的是用括號括起來,前面加反斜杠():
“Call” + “123-000-(indexPath.row)”
Quick note:在 Playgrounds 章已經(jīng)介紹過字符串的串聯(lián)撒轮。如果你翻到第二章的聯(lián)系汹来,是時候再次訪問它了∫称瑁或者胖替,你可以參考附件。
隨著 Call 動作的實(shí)現(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 里的可選項(xiàng)
你可能想知道問號是做什么用的。單元格在 Swift 里被認(rèn)為是一個可選項(xiàng)舍败。在 Swift 里介紹了一個新的格式叫做可選項(xiàng)(Optional)招狸。可選項(xiàng)簡單的意思是“這里有一個值”或者“這里根本沒有值”邻薯。單元格通過 tableView 返回裙戏。cellForRowAtIndexPath 是一個可選項(xiàng)。用問號來訪問 accessoryType 單元格的特性厕诡。在這種情況下累榜,Swift 將檢查單元格是否存在,如果單元格存在灵嫌,允許你來設(shè)置 accessoryType 的值壹罚。在大多數(shù)情況下,當(dāng)你訪問一個可選的屬性時寿羞,Xcode 的自動補(bǔ)全特性會為你添加問號猖凛。為了學(xué)習(xí)更多的關(guān)于可選項(xiàng),你可以進(jìn)一步的參考附錄稠曼。
當(dāng)一個用戶選擇”I’ve been here”選項(xiàng)形病,我們添加給選中的單元格添加一個復(fù)選框。在 table view 單元格里霞幅,右邊部分是留給輔助視圖的漠吻。有4種類型的內(nèi)置輔助視圖包括展開指示器 (disclosure indicator),詳情展開按鈕(detail disclosure button)司恳,復(fù)選框(checkmark)和細(xì)節(jié)(detail)途乃。在這種情況下,我們使用 checkmark 作為指示器扔傅。
代碼塊的第一行使用 indexPath 檢索所選的單元格耍共,它包括了所選單元格的索引烫饼。第二行代碼用一個復(fù)選標(biāo)記更新了 accessoryType 單元格的性質(zhì)。
編譯運(yùn)行 app试读。按一個餐廳然后選擇其中一個行為杠纵,它將展現(xiàn)一個復(fù)選標(biāo)記或者警告給你。
現(xiàn)在钩骇,當(dāng)你選擇一行比藻,高亮顯示灰色和保持選中的行。在 tableView(_:didSelectRowAtIndexPath:) 方法的最后添加下面的代碼來取消選定的行倘屹。
tableView.deselectRowAtIndexPath(indexPath, animated: false)
我們遇到了 Bug
app 看起來很好银亲。但是如果你近距離觀察它,app里有一個 bug纽匙。你用’I’ve been here'標(biāo)記了’Cafe Deadend’餐廳务蝠。如果你往下滾,你會找到另一個餐廳(如Palomino Espresso)同樣包含一個復(fù)選框烛缔。發(fā)生了什么問題馏段?為什么 app 會添加額外的復(fù)選框?
像每個程序員一樣力穗,我討厭 bug 尤其是當(dāng)面臨一個工程快要交貨的時候毅弧。但是 bug 總是能幫助我提高我的編程技巧。如果你繼續(xù)學(xué)習(xí)你也會遇到很多 bug当窗。習(xí)慣它吧够坐。
出現(xiàn)這個問題是由于單元格被重復(fù)使用,這個我們在之前的章節(jié)已經(jīng)討論過了崖面。例如元咙,table view 有30個單元格。由于性能原因巫员,UITableView 可能只創(chuàng)造了10個單元格庶香,當(dāng)你滾動表的時候來重復(fù)使用他們,來代替創(chuàng)造30個單元格简识。這種情況下赶掖,UITableView 重復(fù)使用第一個單元格(最初當(dāng)做一個復(fù)選框用于Cafe Deadend)來顯示另一個餐廳。在我們的代碼里七扰,當(dāng) table view 重復(fù)使用同樣的單元格時奢赂,我們僅僅更新了圖片視圖和標(biāo)簽。附屬視圖并沒有更新颈走。因此膳灶,下一個餐廳重復(fù)使用同樣的單元格共用同樣的附屬視圖。如果附屬視圖包含一個復(fù)選框,那個餐廳同樣帶著一個復(fù)選框轧钓。
我們?nèi)绾谓鉀Q這個 bug序厉?
我們必須找到另一種方式來跟蹤檢查項(xiàng)。創(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)的餐廳被標(biāo)記為”I’ve been here”牺堰。例如,我們可以觀察 restaurantIsVisited[0]的值來Cafe Deadend 是否已經(jīng)被檢查或者沒有颅围。
數(shù)組里的值被初始化為 false伟葫。換句話說,條目默認(rèn)是沒有檢查的院促。上面的代碼行用重復(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。首先常拓,當(dāng)一個餐廳被檢查時我們需要更新 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)在,再次編譯運(yùn)行 app〉嗨。現(xiàn)在你的 bug 應(yīng)該解決了拖陆。
你可以進(jìn)一步使用三元條件運(yùn)算符來把上面的 if 條件簡化成一行代碼(?:):
cell.accessoryType = restaurantIsVisited[indexPath.row] ? .Checkmark : .None
三元條件運(yùn)算符是為評估簡單的條件做的一個高效的速寫。