在本系列Storyboard教程的第一部分,我們已經(jīng)學(xué)習(xí)了如何使用Interface Builder創(chuàng)建并連接不同的視圖控制器,還有如何直接在Storyboard編輯器中創(chuàng)建自定義表項(xiàng)。
本教程的第二部分,也是最終部分,內(nèi)容包括segue(轉(zhuǎn)場),static table view cell(靜態(tài)表項(xiàng)),添加玩家頁面和游戲選擇頁面!
好,現(xiàn)在讓我們一起探索Storyboard的其他酷炫特性吧!
轉(zhuǎn)場(Segue)
讓我們向Storyboard中繼續(xù)添加視圖控制器,創(chuàng)建一個(gè)讓用戶添加新玩家的頁面。
打開Main.storyboard,在包含表視圖的那個(gè)Players場景的導(dǎo)航欄右側(cè)拖入一個(gè)Bar Button Item(欄按鈕項(xiàng)),在屬性檢查器中將Identifier設(shè)為Add,使其成為標(biāo)準(zhǔn)添加(加號)按鈕。

當(dāng)用戶點(diǎn)按這個(gè)按鈕時(shí),你希望App會彈出一個(gè)模態(tài)頁面讓用戶輸入新玩家的詳細(xì)信息。
在Players場景的右邊拖入一個(gè)新的Navigation Controller(導(dǎo)航控制器)。記得雙擊面板可以縮放畫面騰出空間。新加入的導(dǎo)航控制器附帶一個(gè)表視圖控制器,很方便。
這里有個(gè)小技巧:選擇剛才在Players頁面里加入的加號按鈕,按住control鍵把它拖向新建的導(dǎo)航控制器,松手,在彈出的小選單中選擇modal(模態(tài))。
還記得嗎:當(dāng)Storyboard面板處于縮小狀態(tài)時(shí),無法添加或修改內(nèi)容。如果在創(chuàng)建轉(zhuǎn)場時(shí)遇到問題,請嘗試雙擊放大!

現(xiàn)在Players頁面和導(dǎo)航控制器之間多了一個(gè)新箭頭。

這種連接的類型叫做segue(轉(zhuǎn)場,讀作seg-way,源自電影術(shù)語,原指兩個(gè)場景間的過渡銜接),表示一個(gè)頁面到另一個(gè)頁面的過渡。此前我們所見的Storyboard連接描述的都是視圖控制器的包含關(guān)系,而轉(zhuǎn)場是用來切換頁面的。轉(zhuǎn)場可以由點(diǎn)擊按鈕、表項(xiàng)、手勢等條件觸發(fā)。
使用轉(zhuǎn)場的好處是,再也不用為呈現(xiàn)新頁面寫代碼了,也不用把按鈕連接到IBAction方法上,你只需要在Storyboard中從一個(gè)欄按鈕項(xiàng)拖到下一個(gè)頁面就可以創(chuàng)建過渡了。(注:如果你的控件已經(jīng)綁定了IBAction連接,該連接會被轉(zhuǎn)場屏蔽。)
運(yùn)行App,點(diǎn)擊加號按鈕,一個(gè)新的表視圖會從屏幕下方滑入。

這就是所謂的模態(tài)轉(zhuǎn)場。新頁面完全覆蓋原頁面,在關(guān)閉模態(tài)頁面之前,用戶只能在新頁面進(jìn)行交互。后面我們還會看到push(入棧)轉(zhuǎn)場,這種轉(zhuǎn)場會把新頁面壓入導(dǎo)航控制器的導(dǎo)航棧(navigation stack)。
現(xiàn)在新頁面還沒什么用,連關(guān)閉頁面返回都做不到,有去無回,因?yàn)檗D(zhuǎn)場是單向操作。
為返回頁面,Storyboard提供了unwind(回退)轉(zhuǎn)場。接下來我們要實(shí)現(xiàn)返回功能,主要分三個(gè)步驟:
創(chuàng)建讓用戶點(diǎn)選的控件,通常是個(gè)按鈕。
在你想返回的控制器創(chuàng)建回退方法。
在Storyboard中將控件與回退方法連接。
首先打開Main.storyboard,選擇新的表視圖控制器場景(叫“Root View Controller”的那個(gè))。雙擊導(dǎo)航欄,把標(biāo)題改成“Add Player”。然后在導(dǎo)航欄添加兩個(gè)欄按鈕項(xiàng),在屬性檢查器中設(shè)置左側(cè)按鈕的Identifier為Cancel,右側(cè)按鈕為Done,并將右側(cè)按鈕的Style改成Done。

接下來在項(xiàng)目中用Cocoa Touch Class模板添加一個(gè)新文件,命名為PlayerDetailsViewController并令其繼承
UITableViewController。要把這個(gè)類關(guān)聯(lián)到Storyboard,先切回Main.storyboard,選擇添加玩家的場景,然后在身份檢查器(Identity inspector)中設(shè)Class為PlayerDetailsViewController。這個(gè)步驟我經(jīng)常忘掉,在此特地提醒,還請讀者牢記。
現(xiàn)在終于可以創(chuàng)建回退轉(zhuǎn)場了。在PlayersViewController.swift(不是detail那個(gè))的類定義下面添加如下的回退方法:
@IBAction func cancelToPlayersViewController(segue:UIStoryboardSegue) {
dismissViewControllerAnimated(true, completion: nil)
}
@IBAction func savePlayerDetail(segue:UIStoryboardSegue) {
dismissViewControllerAnimated(true, completion: nil)
}
這兩個(gè)方法在調(diào)用時(shí)都會解除這個(gè)控制器。后面你會改寫savePlayerDetail,讓它名副其實(shí)地履行自己的職責(zé)。
最后回到Interface Builder,把Cancel按鈕和Done按鈕連接到相應(yīng)的action方法上。按住control從欄按鈕拖到視圖控制器上面的出口(exit)對象上,然后從彈出的選單中選擇正確的action名稱。

記住取消方法的方法名,創(chuàng)建回退轉(zhuǎn)場時(shí),App中的所有回退方法(形如
@IBAction func methodname(segue:UIStoryboardSegue))都會在列表中顯示,所以命名方法時(shí)要多加注意,避免混淆。
運(yùn)行App,點(diǎn)擊加號按鈕,然后測試Cancel和Done按鈕。僅僅幾行代碼就可以實(shí)現(xiàn)如此功能。
靜態(tài)表項(xiàng)(Static Cell)
完成這部分后,添加玩家頁面會像這樣:

當(dāng)然這是一個(gè)分組表視圖(grouped table view),但不必為該表創(chuàng)建數(shù)據(jù)源,也不必為此編寫
cellForRowAtIndexPath方法,你可以直接在Interface Builder中完成設(shè)計(jì)。這個(gè)特性叫做靜態(tài)表項(xiàng)(static cell)。
選中Add Player場景的表視圖,在屬性檢查器中設(shè)Content為Static Cells,把Style由Plain改成Grouped,并為表視圖設(shè)置兩個(gè)分段(section)。

修改Sections屬性值時(shí),編輯器會復(fù)制已有的分段。(你也可以在左側(cè)的文檔大綱中選擇特定分段并復(fù)制。)
最終頁面每個(gè)分段應(yīng)該只有一行,請?jiān)诿姘寤蛭臋n大綱中選中并刪除多余的表項(xiàng)。
在文檔大綱中選擇最上面的表視圖分段,在屬性選擇器中設(shè)Header字段值為Player Name。

向該分段內(nèi)拖入一個(gè)新的Text Field(文本字段),橫向拉長并移除邊框,使文本字段控件融入周圍環(huán)境。設(shè)字體為 System 17.0 ,勾掉Adjust to Fit選項(xiàng)。

接下來我們要用Xcode的Assistant Editor(輔助編輯器)功能為該文本字段在
PlayerDetailsViewController中創(chuàng)建一個(gè)outlet。在Storyboard中,點(diǎn)擊工具欄上的按鈕(圖標(biāo)是兩個(gè)套在一起的圓圈)打開輔助編輯器,應(yīng)該會自動打開PlayerDetailsViewController.swift(如果沒有,在右側(cè)的跳轉(zhuǎn)欄中選擇相應(yīng)文件)。
選擇新建的文本字段,按住control拖到swift文件的類定義下面。在彈出框中將新outlet命名為nameTextField并點(diǎn)擊Connect。在點(diǎn)擊Connect后Xcode會在PlayersDetailViewController類中添加屬性并在Storyboard中建立連接:

為表項(xiàng)上的視圖創(chuàng)建outlet對于原型表項(xiàng)來說可能會遇到問題,這在上一部分的教程中提到過,不過靜態(tài)表項(xiàng)就不必?fù)?dān)心了,因?yàn)槊總€(gè)靜態(tài)表項(xiàng)都只會有唯一的實(shí)例,把子視圖與視圖控制器的outlet連接完全沒問題。
把第二分段的靜態(tài)表項(xiàng)的Style設(shè)為Right Detail,這會套用一個(gè)標(biāo)準(zhǔn)表項(xiàng)樣式,雙擊左側(cè)的label,把文本改為Game,然后為該表項(xiàng)設(shè)定Disclosure Indicator(展開方向標(biāo))附件。

仿照剛才的Name文本字段,為右面的label("Detail"的那個(gè))創(chuàng)建outlet并命名為detailLabel,該表項(xiàng)上的label都是常規(guī)
UILabel對象。在建立連接前選擇Detail文本字段時(shí)可能需要多次點(diǎn)擊,請確保選擇的是label而不是整個(gè)表項(xiàng)。完成后如圖:

添加玩家頁面的最終設(shè)計(jì)效果如圖:

目前在Storyboard中設(shè)計(jì)的頁面尺寸都符合iPhone 5的4英寸屏幕,高度為568點(diǎn)。當(dāng)然你的App應(yīng)當(dāng)在不同的屏幕尺寸下正常工作,你可以在Storyboard中預(yù)覽所有的尺寸。
在工具欄上點(diǎn)開輔助編輯器,選擇跳轉(zhuǎn)欄中的Preview。點(diǎn)擊輔助編輯器左下角的加號添加新的預(yù)覽尺寸,如果想刪除一個(gè)屏幕尺寸,選中并按delete鍵即可。

一個(gè)簡單的評分App不需要什么花哨的東西,只是使用表視圖控制器,頁面自動縮放以填滿屏幕空間。當(dāng)你想為不同的屏幕尺寸適配布局時(shí),你需要使用Auto Layout和Size Classes。
構(gòu)建并運(yùn)行App,你會注意到添加玩家頁面依然是空白!

表視圖控制器在使用靜態(tài)表項(xiàng)時(shí)不需要數(shù)據(jù)源,而之前你用Xcode模板創(chuàng)建的
PlayerDetailsViewController類中依然有部分?jǐn)?shù)據(jù)源相關(guān)代碼,靜態(tài)表項(xiàng)因此無法正常工作,所以靜態(tài)內(nèi)容沒有顯示出來。我們這就來解決問題!
打開PlayerDetailsViewController.swift文件,刪除這一條代碼往下的所有內(nèi)容(注意不要?jiǎng)h掉類自己的括號):
// MARK: - Table view data source
現(xiàn)在,自從加入這個(gè)類以后Xcode顯示的那幾條警告(warning)也應(yīng)該消失了。
運(yùn)行App,檢查使用靜態(tài)表項(xiàng)的新頁面。完全沒有寫代碼,其實(shí)剛才還刪了一段代碼!
還要了解一點(diǎn):靜態(tài)表項(xiàng)只在UITableViewController中有效,雖然Interface Builder允許你在常規(guī)UIViewController中的表視圖對象里添加靜態(tài)表項(xiàng),運(yùn)行時(shí)不會發(fā)揮作用,原因是UITableViewController中額外實(shí)現(xiàn)了一些用來處理靜態(tài)表項(xiàng)數(shù)據(jù)源的操作。在項(xiàng)目中誤用的話Xcode甚至?xí)芙^編譯,輸出報(bào)錯(cuò)信息:“Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances”。
另一方面,原型表項(xiàng)在常規(guī)視圖內(nèi)的表視圖中可以正常工作,但在nib中就沒戲了。目前來講,使用原型表項(xiàng)或靜態(tài)表項(xiàng)就必須使用Storyboard。
你也有可能想在一個(gè)表視圖中混合使用靜態(tài)表項(xiàng)和常規(guī)的動態(tài)表項(xiàng),很遺憾的是目前的SDK對此支持欠佳。如果你的App有這種需求,請參考蘋果開發(fā)者官方論壇上的相關(guān)帖子尋求可行方案。
注:如果構(gòu)建的頁面上包含的靜態(tài)表項(xiàng)多到無法在可視范圍內(nèi)全部展示,你可以在Interface Builder中直接利用滾動手勢查看,這個(gè)功能可能不容易發(fā)現(xiàn),但確實(shí)管用。
不過總的來說該寫代碼的地方只能靠代碼,甚至靜態(tài)表項(xiàng)的表視圖也是如此。前面在把文本字段拖進(jìn)第一個(gè)表項(xiàng)的時(shí)候,你可能發(fā)現(xiàn)尺寸不大合適,文本字段周圍有一點(diǎn)白邊,而且用戶看不到文本字段的實(shí)際范圍,如果正好點(diǎn)在邊框上,沒有彈出鍵盤,用戶會感到困惑。
為避免這種情況,你應(yīng)該讓那一行任意位置接受的點(diǎn)擊都可以喚出鍵盤。要這樣做很容易,打開PlayerDetailsViewController.swift并如下添加
tableView(_:didSelectRowAtIndexPath:)`方法:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 0 {
nameTextField.becomeFirstResponder()
}
}
代碼的意思是如果用戶點(diǎn)按第一個(gè)表項(xiàng),App應(yīng)該激活相應(yīng)文本字段。該分段只有一個(gè)表項(xiàng),你只需使用分段的索引。設(shè)文本字段為第一響應(yīng)者會自動喚出鍵盤。這只是一小處用戶體驗(yàn)優(yōu)化,但就是這樣一個(gè)小細(xì)節(jié)可以給用戶省去一點(diǎn)煩惱。
小訣竅:添加delegate委托方法或重寫視圖控制器方法時(shí),直接輸入方法名開頭的幾個(gè)字母(前面不加func),即可在自動補(bǔ)全列表中選擇正確的方法。
另外,還應(yīng)該在Storyboard的屬性檢查器中把相應(yīng)表項(xiàng)的Selection Style設(shè)為None(原本是Default),否則用戶點(diǎn)按文本字段周圍的邊框時(shí)該行會高亮。

好啦,添加玩家頁面設(shè)計(jì)完成。現(xiàn)在我們要實(shí)現(xiàn)功能。
為添加玩家頁面實(shí)現(xiàn)功能
現(xiàn)在先不管Game這行,只輸入玩家名稱。
當(dāng)用戶點(diǎn)擊Cancel按鈕時(shí),頁面關(guān)閉,用戶剛剛輸入的數(shù)據(jù)隨之作廢。這部分功能直接用回退轉(zhuǎn)場已經(jīng)實(shí)現(xiàn)好了。
而當(dāng)用戶點(diǎn)擊Done時(shí),你應(yīng)該創(chuàng)建一個(gè)新的Player對象,參照用戶輸入填充屬性后更新玩家列表。
轉(zhuǎn)場即將發(fā)生時(shí),prepareForSegue(:sender:)會被調(diào)用。你可以重寫這個(gè)方法,在退出視圖之前將數(shù)據(jù)保存到一個(gè)新的Player對象中。
注:不要擅自調(diào)用prepareForSegue方法,這是UIKit通知你一個(gè)轉(zhuǎn)場剛剛被觸發(fā)的消息。
在PlayerDetailsViewController.swift中,先在類上添加一條屬性:
var player:Player!
這條語句并不會將屬性實(shí)例化,但其中的感嘆號把該變量定義為隱式解包可選量(implicitly unwrapped optional),意思是該變量必須被實(shí)例化,而且你確定它在被使用前一定有值。
接下來在PlayerDetailsViewController.swift中添加以下方法:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SavePlayerDetail" {
player = Player(name: self.nameTextField.text, game: "Chess", rating: 1)
}
}
prepareForSegue(_:sender:)方法判斷轉(zhuǎn)場的標(biāo)識符是否為SavePlayerDetail,當(dāng)且僅當(dāng)判定結(jié)果為真時(shí),創(chuàng)建一個(gè)新的Player實(shí)例,其中g(shù)ame和rating均取默認(rèn)值。如果此時(shí)運(yùn)行,App會崩潰,因?yàn)椴淮嬖跇?biāo)識符SavePlayerDetail`,player不會被實(shí)例化,結(jié)合前面的隱式解包可選量定義,引發(fā)運(yùn)行時(shí)錯(cuò)誤。
小提示:如果App出現(xiàn)詭異的崩潰問題,而且代碼看起來似乎并無邏輯錯(cuò)誤,那么可能是在代碼中刪除過對象或修改過對象名,以致Storyboard引用對象出錯(cuò)。
在Main.storyboard中,在文檔大綱里找到Add Player場景,選擇連接到savePlayerDetail這個(gè)action的回退轉(zhuǎn)場,將其標(biāo)識符改為SavePlayerDetail:

然后選擇連接到
cancelToPlayersViewController的回退轉(zhuǎn)場,將其標(biāo)識符改為CancelPlayerDetail。以供prepareForSegue(_:sender:)方法判斷標(biāo)識符。
轉(zhuǎn)到PlayersViewController類,如下修改回退轉(zhuǎn)場方法savePlayerDetail(segue:):
@IBAction func savePlayerDetail(segue:UIStoryboardSegue) {
let playerDetailsViewController = segue.sourceViewController as PlayerDetailsViewController
//add the new player to the players array
players.append(playerDetailsViewController.player)
//update the tableView
let indexPath = NSIndexPath(forRow: players.count-1, inSection: 0)
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
//hide the detail view controller
dismissViewControllerAnimated(true, completion: nil)
}
這會通過傳入方法的轉(zhuǎn)場引用獲取一個(gè)指向PlayerDetailsViewController的引用,并借此向數(shù)據(jù)源中使用的Player數(shù)組添加新的Player對象,然后通知表視圖在末尾新增了一行,因?yàn)楸硪晥D和數(shù)據(jù)源應(yīng)當(dāng)保持同步。
你可能會直接調(diào)用tableView.reloadData(),但還是為新行插入的操作加入動畫效果比較好。UITableViewRowAnimation.Automatic會以插入新行的位置自動選用合適的動畫,十分方便。
試試看,現(xiàn)在應(yīng)該可以向列表中加入新玩家了!

性能
現(xiàn)在Storyboard已經(jīng)有好幾個(gè)視圖控制器了,你或許會擔(dān)心性能問題,不過一次載入整個(gè)Storyboard并不是什么苦活,Storyboard不會立即實(shí)例化所有的視圖控制器,立即載入的只有初始視圖控制器。而由于這里的初始視圖控制器是一個(gè)分頁欄控制器,包含的兩個(gè)視圖控制器也會被載入(第一個(gè)分頁標(biāo)簽的Players場景和第二個(gè)分頁標(biāo)簽的場景)。
其他視圖控制器只有在轉(zhuǎn)場過去的時(shí)候才會被實(shí)例化。而當(dāng)關(guān)閉視圖控制器的時(shí)候,它們會立即被釋放,所以內(nèi)存中只有活躍使用的視圖控制器,就好像分別使用nib一樣。
實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn),在PlayerDetailsViewController類中添加構(gòu)造器(initializer)和析構(gòu)器(deinitializer):
required init(coder aDecoder: NSCoder) {
println("init PlayerDetailsViewController")
super.init(coder: aDecoder)
}
deinit {
println("deinit PlayerDetailsViewController")
}
你剛剛重寫了init(coder:)和deinit方法,讓它們向Xcode調(diào)試面板輸出信息?,F(xiàn)在運(yùn)行App,打開添加玩家頁面,你會發(fā)現(xiàn)視圖控制器只有在被打開的時(shí)候才會分配。
關(guān)閉添加玩家頁面的時(shí)候,無論是點(diǎn)擊Cancel還是Done都會看到deinit析構(gòu)器的println()輸出。如果再次打開這個(gè)頁面,你還會看到init(coder:)的輸出,這樣你應(yīng)該相信這個(gè)事實(shí)了:視圖控制器是按需加載的,就像手動載入nib一樣。
注:如果你以前用過nib,那么你應(yīng)該會很熟悉構(gòu)造器init(coder:),這部分機(jī)制延續(xù)到了Storyboard中:使用的方法依然是init(coder:),awakeFromNib()和viewDidLoad()。Storyboard可以看成附帶了過渡信息和關(guān)聯(lián)信息的一系列nib的集合,而Storyboard內(nèi)的視圖和視圖控制器使用與nib相同的方式編碼并解析。
游戲選擇頁面
在添加玩家頁面中點(diǎn)選Game行應(yīng)該打開一個(gè)新頁面并讓用戶從列表中選擇一個(gè)游戲,這意味著下一步要加入另外一個(gè)表視圖控制器,不過這次的頁面不是模態(tài)顯示,而是壓入導(dǎo)航棧。
向Storyboard中拖入一個(gè)新的表視圖控制器,在添加玩家頁面中選擇Game表項(xiàng)(確保選中的是整個(gè)表項(xiàng),而不是其中的label),然后按住control拖到新建的表視圖控制器,在兩者之間創(chuàng)建轉(zhuǎn)場。在彈出的選單中選擇轉(zhuǎn)場類型為Push,然后在屬性檢查器中把轉(zhuǎn)場的Identifier標(biāo)識符設(shè)為PickGame。
雙擊導(dǎo)航欄,將新場景命名為Choose Game。設(shè)原型表項(xiàng)的Style為Basic(基本),設(shè)重用標(biāo)識符為GameCell,如圖:

在項(xiàng)目中使用Cocoa Touch Class模板新建一個(gè)Swift文件,命名為GamePickerViewController,繼承UITableViewController?;氐絊toryboard中將游戲選擇頁面的Custom Class設(shè)為
GamePickerViewController。
現(xiàn)在為新頁面添加數(shù)據(jù)。在GamePickerViewController.swift中,在開頭添加games屬性,然后重寫viewDidLoad函數(shù),像這樣:
var games:[String]!
override func viewDidLoad() {
super.viewDidLoad()
games = ["Angry Birds",
"Chess",
"Russian Roulette",
"Spin the Bottle",
"Texas Hold'em Poker",
"Tic-Tac-Toe"]
}
你剛剛新增了一個(gè)叫做games的字符串?dāng)?shù)組,并在viewDidLoad()中用寫定的內(nèi)容填充數(shù)組。
然后如下替換數(shù)據(jù)源方法:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return games.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("GameCell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = games[indexPath.row]
return cell
}
上述代碼將games數(shù)組設(shè)為數(shù)據(jù)源并替換表項(xiàng)的textLabel中的字符串值。
只要數(shù)據(jù)源準(zhǔn)備就緒就應(yīng)該能正常工作。運(yùn)行App,點(diǎn)選Game行,新的游戲選擇頁面會滑入屏幕?,F(xiàn)在點(diǎn)擊各項(xiàng)不會有什么效果,但由于該頁面呈現(xiàn)在導(dǎo)航棧上,你可以直接點(diǎn)擊返回按鈕,返回原來的添加玩家頁面。

不用寫代碼就可以喚出新頁面,是不是很贊?只要按住control從靜態(tài)表項(xiàng)拖到新場景,寫的代碼只有填充表視圖的內(nèi)容,而且一般來講比原地設(shè)計(jì)好的列表要靈活些(因?yàn)間ames數(shù)組更方便修改)。
當(dāng)然新頁面要返回?cái)?shù)據(jù)才有用,為此你要添加一個(gè)新的回退轉(zhuǎn)場。
在GamePickerViewController類的上面添加持有選中的游戲的名稱和索引的屬性:
var selectedGame:String? = nil
var selectedGameIndex:Int? = nil
然后修改cellForRowAtIndexPath::
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("GameCell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = games[indexPath.row]
if indexPath.row == selectedGameIndex {
cell.accessoryType = .Checkmark
} else {
cell.accessoryType = .None
}
return cell
}
這會在當(dāng)前所選游戲?qū)?yīng)的表項(xiàng)附上選中標(biāo)記(對號),這對用戶體驗(yàn)來說不可或缺。
接著添加tableview(tableview:didSelectRowAtIndexPath:)方法:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
//Other row is selected - need to deselect it
if let index = selectedGameIndex {
let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0))
cell?.accessoryType = .None
}
selectedGameIndex = indexPath.row
selectedGame = games[indexPath.row]
//update the checkmark for the current row
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.accessoryType = .Checkmark
}
這段代碼首先會取消選擇剛剛點(diǎn)選的行,外觀會從灰色高亮變回常規(guī)的白色,然后移除對號,并在剛剛點(diǎn)選的行上附加選中標(biāo)記。
運(yùn)行App,測試是否正常。點(diǎn)選一個(gè)游戲名,相應(yīng)行會附上選中標(biāo)記,點(diǎn)選另一個(gè)游戲名,選中標(biāo)記也隨之移動。

按要求來說點(diǎn)選某行之后應(yīng)該關(guān)閉該頁面,不過現(xiàn)在并沒有自動返回,因?yàn)樯形唇壎ɑ赝宿D(zhuǎn)場。
在PlayerDetailsViewController.swift的類上面添加一個(gè)持有被選游戲的屬性,以便之后在Player對象中保存。令其默認(rèn)值為"Chess",這樣一來新玩家總會有一個(gè)選定的游戲。
var game:String = "Chess"
同樣在該文件中改寫viewDidLoad()以在靜態(tài)表項(xiàng)中游戲名稱:
override func viewDidLoad() {
super.viewDidLoad()
detailLabel.text = game
}
添加回退轉(zhuǎn)場方法:
@IBAction func selectedGame(segue:UIStoryboardSegue) {
let gamePickerViewController = segue.sourceViewController as GamePickerViewController
if let selectedGame = gamePickerViewController.selectedGame {
detailLabel.text = selectedGame
game = selectedGame
}
self.navigationController?.popViewControllerAnimated(true)
}
上述代碼會在用戶從選擇游戲場景選中一個(gè)游戲后執(zhí)行。該方法按照選中的游戲更新頁面上的label和game屬性,然后將GamePickerViewController彈出導(dǎo)航棧。
在Main.storyboard中按住control從表項(xiàng)拖到Exit出口對象,然后從彈出列表中選擇selectedGame:。

設(shè)該回退轉(zhuǎn)場標(biāo)識符為SaveSelectedGame。
運(yùn)行App試試看,創(chuàng)建新玩家,點(diǎn)選Game行并選擇一個(gè)游戲。

不幸的是,這個(gè)回退轉(zhuǎn)場方法是在
tableView(_:didSelectRowAtIndexPath:)方法前執(zhí)行的,所以selectedGameIndex并未及時(shí)更新。幸運(yùn)的是你可以重寫prepareForSegue(_:sender:)方法,在轉(zhuǎn)場之前完成更新操作。
在GamePickerViewController中添加prepareForSegue(segue:)方法:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SaveSelectedGame" {
let cell = sender as UITableViewCell
let indexPath = tableView.indexPathForCell(cell)
selectedGameIndex = indexPath?.row
if let index = selectedGameIndex {
selectedGame = games[index]
}
}
}
prepareForSegue(_:sender:)的sender參數(shù)是引發(fā)轉(zhuǎn)場的對象,在這里對應(yīng)選中的游戲表項(xiàng),所以你可以利用表項(xiàng)的indexPath來在games數(shù)組中確定選中的游戲并在轉(zhuǎn)場發(fā)生之前更新selectedGame。
現(xiàn)在運(yùn)行App,選擇游戲后玩家的游戲信息會隨之更新了。

接下來改寫PlayerDetailsViewController的prepareForSegue方法來返回選中的游戲,而不是寫定的"Chess"。這樣一來,完成添加玩家的操作后,Players場景中會顯示玩家實(shí)際選擇的游戲。
在PlayerDetailsViewController.swift中如下改寫prepareForSegue(_:sender:)方法:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SavePlayerDetail" {
player = Player(name: nameTextField.text, game:game, rating: 1)
}
}
完成添加玩家頁面并點(diǎn)擊Done后,玩家列表會更新正確的游戲信息。
還有一點(diǎn),當(dāng)你選擇一個(gè)游戲,返回添加玩家頁面,然后嘗試重新選擇游戲的時(shí)候,之前選定的游戲應(yīng)該顯示選中標(biāo)記。解決方法是在轉(zhuǎn)場時(shí)把PlayerDetailsViewController中保存的選中的游戲傳給GamePickerViewController。
還是在PlayerDetailsViewController.swift中,于prepareForSegue(segue:,sender:)方法的末尾添加以下代碼:
if segue.identifier == "PickGame" {
let gamePickerViewController = segue.destinationViewController as GamePickerViewController
gamePickerViewController.selectedGame = game
}
注意:現(xiàn)在有兩條檢查segue.identifier的if語句。SavePlayerDetail是返回玩家列表的回退轉(zhuǎn)場,PickGame是前往游戲選擇頁面的入棧轉(zhuǎn)場。添加的代碼會在GamePickerViewController的視圖加載之前更新其中的selectedGame。
打開GamePickerViewController.swift并在viewDidLoad()末尾添加以下代碼:
if let game = selectedGame {
selectedGameIndex = find(games, game)!
}
這兩行代碼獲取從PlayerDetailsViewController傳進(jìn)的selectedGame并將其轉(zhuǎn)換成正確的索引。find()函數(shù)會在games數(shù)組中查找匹配selectedGame的String,然后返回匹配元素的索引,賦值給selectedGameIndex,這個(gè)索引用來在對應(yīng)表項(xiàng)上設(shè)置選中標(biāo)記。
好?,F(xiàn)在選擇游戲頁面功能實(shí)現(xiàn)完成!

何去何從?
可喜可賀,現(xiàn)在你已經(jīng)了解Storyboard編輯器的基本用法,能夠創(chuàng)建包含多個(gè)視圖控制器并能通過轉(zhuǎn)場在場景之間切換的App!在一處集中管理多個(gè)視圖控制器和互相的關(guān)聯(lián),讓整體把握App的樣子更加容易。
你也看到了自定義表視圖和表項(xiàng)有多么容易。有了靜態(tài)表項(xiàng),不用實(shí)現(xiàn)所有的數(shù)據(jù)源方法也可以構(gòu)建一些界面。