天天看點

Swift Swift語言Storyboard教程:第二部

本文由CocoaChina翻譯小組​​@TurtleFromMars​​翻譯自raywenderlich​

2014/12/5更新:更新至 Xcode 6.2 Beta。

如果你想學習Storyboard,你來對地方了!

在​​本系列Storyboard教程的第一部分​​,我們已經學習了如何使用Interface Builder建立并連接配接不同的視圖控制器,還有如何直接在Storyboard編輯器中建立自定義表項。

本教程的第二部分,也是最終部分,内容包括segue(轉場),static table view cell(靜态表項),添加玩家頁面和遊戲選擇頁面!

我們從上部分結束的地方開始,請先打開之前的項目,或者下載下傳​​上半部分教程的示例代碼​​。

好,現在讓我們一起探索Storyboard的其他酷炫特性吧!

轉場(Segue)

讓我們向Storyboard中繼續添加視圖控制器,建立一個讓使用者添加新玩家的頁面。

打開Main.storyboard,在包含表視圖的那個Players場景的導航欄右側拖入一個Bar Button Item(欄按鈕項),在屬性檢查器中将Identifier設為Add,使其成為标準添加(加号)按鈕。

Swift Swift語言Storyboard教程:第二部

當使用者點按這個按鈕時,你希望App會彈出一個模态頁面讓使用者輸入新玩家的詳細資訊。

在Players場景的右邊拖入一個新的Navigation Controller(導航控制器)。記得輕按兩下面闆可以縮放畫面騰出空間。新加入的導航控制器附帶一個表視圖控制器,很友善。

這裡有個小技巧:選擇剛才在Players頁面裡加入的加号按鈕,按住control鍵把它拖向建立的導航控制器,松手,在彈出的小選單中選擇modal(模态)。

還記得嗎:當Storyboard面闆處于縮小狀态時,無法添加或修改内容。如果在建立轉場時遇到問題,請嘗試輕按兩下放大!

Swift Swift語言Storyboard教程:第二部

現在Players頁面和導航控制器之間多了一個新箭頭。

Swift Swift語言Storyboard教程:第二部

這 種連接配接的類型叫做segue(轉場,讀作seg-way,源自電影術語,原指兩個場景間的過渡銜接),表示一個頁面到另一個頁面的過渡。此前我們所見的 Storyboard連接配接描述的都是視圖控制器的包含關系,而轉場是用來切換頁面的。轉場可以由點選按鈕、表項、手勢等條件觸發。

使用轉場的好處是,再也不用為呈現新頁面寫代碼了,也不用把按鈕連接配接到IBAction方法上,你隻需要在Storyboard中從一個欄按鈕項拖到下一個頁面就可以建立過渡了。(注:如果你的控件已經綁定了IBAction連接配接,該連接配接會被轉場屏蔽。)

運作App,點選加号按鈕,一個新的表視圖會從螢幕下方滑入。

Swift Swift語言Storyboard教程:第二部

這就是所謂的模态轉場。新頁面完全覆寫原頁面,在關閉模态頁面之前,使用者隻能在新頁面進行互動。後面我們還會看到push(入棧)轉場,這種轉場會把新頁面壓入導航控制器的導航棧(navigation stack)。

現在新頁面還沒什麼用,連關閉頁面傳回都做不到,有去無回,因為轉場是單向操作。

為傳回頁面,Storyboard提供了unwind(回退)轉場。接下來我們要實作傳回功能,主要分三個步驟:

1.  建立讓使用者點選的控件,通常是個按鈕。

2.  在你想傳回的控制器建立回退方法。

3.  在Storyboard中将控件與回退方法連接配接。

首 先打開Main.storyboard,選擇新的表視圖控制器場景(叫“Root View Controller”的那個)。輕按兩下導航欄,把标題改成“Add Player”。然後在導航欄添加兩個欄按鈕項,在屬性檢查器中設定左側按鈕的Identifier為Cancel,右側按鈕為Done,并将右側按鈕的 Style改成Done。

Swift Swift語言Storyboard教程:第二部

接 下來在項目中用Cocoa Touch Class模闆添加一個新檔案,命名為PlayerDetailsViewController并令其繼承 `UITableViewController`。要把這個類關聯到Storyboard,先切回Main.storyboard,選擇添加玩家的場景, 然後在身份檢查器(Identity inspector)中設Class為PlayerDetailsViewController。這個步驟我經常忘掉,在此特地提醒,還請讀者牢記。

現在終于可以建立回退轉場了。在PlayersViewController.swift(不是detail那個)的類定義下面添加如下的回退方法:

這兩個方法在調用時都會解除這個控制器。後面你會改寫`savePlayerDetail`,讓它名副其實地履行自己的職責。

最後回到Interface Builder,把Cancel按鈕和Done按鈕連接配接到相應的action方法上。按住control從欄按鈕拖到視圖控制器上面的出口(exit)對象上,然後從彈出的選單中選擇正确的action名稱。

Swift Swift語言Storyboard教程:第二部

記住取消方法的方法名,建立回退轉場時,App中的所有回退方法(形如`@IBAction func methodname(segue:UIStoryboardSegue)`)都會在清單中顯示,是以命名方法時要多加注意,避免混淆。

運作App,點選加号按鈕,然後測試Cancel和Done按鈕。僅僅幾行代碼就可以實作如此功能。

靜态表項(Static Cell)

完成這部分後,添加玩家頁面會像這樣:

當 然這是一個分組表視圖(grouped table view),但不必為該表建立資料源,也不必為此編寫`cellForRowAtIndexPath`方法,你可以直接在Interface Builder中完成設計。這個特性叫做靜态表項(static cell)。

選中Add Player場景的表視圖,在屬性檢查器中設Content為Static Cells,把Style由Plain改成Grouped,并為表視圖設定兩個分段(section)。

Swift Swift語言Storyboard教程:第二部

修改Sections屬性值時,編輯器會複制已有的分段。(你也可以在左側的文檔大綱中選擇特定分段并複制。)

最終頁面每個分段應該隻有一行,請在面闆或文檔大綱中選中并删除多餘的表項。

在文檔大綱中選擇最上面的表視圖分段,在屬性選擇器中設Header字段值為Player Name。

Swift Swift語言Storyboard教程:第二部

向該分段内拖入一個新的Text Field(文本字段),橫向拉長并移除邊框,使文本字段控件融入周圍環境。設字型為 _System 17.0_ ,勾掉Adjust to Fit選項。

Swift Swift語言Storyboard教程:第二部

接 下來我們要用Xcode的Assistant Editor(輔助編輯器)功能為該文本字段在`PlayerDetailsViewController`中建立一個outlet。在 Storyboard中,點選工具欄上的按鈕(圖示是兩個套在一起的圓圈)打開輔助編輯器,應該會自動打開 PlayerDetailsViewController.swift(如果沒有,在右側的跳轉欄中選擇相應檔案)。

選擇建立的文本字段, 按住control拖到swift檔案的類定義下面。在彈出框中将新outlet命名為nameTextField并點選Connect。在點選 Connect後Xcode會在PlayersDetailViewController類中添加屬性并在Storyboard中建立連接配接:

為表項上的視圖建立outlet對于原型表項來說可能會遇到問題,這在上一部分的教程中提到過,不過靜态表項就不必擔心了,因為每個靜态表項都隻會有唯一的執行個體,把子視圖與視圖控制器的outlet連接配接完全沒問題。

把第二分段的靜态表項的Style設為Right Detail,這會套用一個标準表項樣式,輕按兩下左側的label,把文本改為Game,然後為該表項設定Disclosure Indicator(展開方向标)附件。

Swift Swift語言Storyboard教程:第二部

仿 照剛才的Name文本字段,為右面的label("Detail"的那個)建立outlet并命名為detailLabel,該表項上的label都是常 規`UILabel`對象。在建立連接配接前選擇Detail文本字段時可能需要多次點選,請確定選擇的是label而不是整個表項。完成後如圖:

Swift Swift語言Storyboard教程:第二部

添加玩家頁面的最終設計效果如圖:

Swift Swift語言Storyboard教程:第二部

目前在Storyboard中設計的頁面尺寸都符合iPhone 5的4英寸螢幕,高度為568點。當然你的App應當在不同的螢幕尺寸下正常工作,你可以在Storyboard中預覽所有的尺寸。

在工具欄上點開輔助編輯器,選擇跳轉欄中的Preview。點選輔助編輯器左下角的加号添加新的預覽尺寸,如果想删除一個螢幕尺寸,選中并按delete鍵即可。

Swift Swift語言Storyboard教程:第二部

一個簡單的評分App不需要什麼花哨的東西,隻是使用表視圖控制器,頁面自動縮放以填滿螢幕空間。當你想為不同的螢幕尺寸适配布局時,你需要使用Auto Layout和Size Classes。

建構并運作App,你會注意到添加玩家頁面依然是空白!

Swift Swift語言Storyboard教程:第二部

表視圖控制器在使用靜态表項時不需要資料源,而之前你用Xcode模闆建立的`PlayerDetailsViewController`類中依然有部分資料源相關代碼,靜态表項是以無法正常工作,是以靜态内容沒有顯示出來。我們這就來解決問題!

打開PlayerDetailsViewController.swift檔案,删除這一條代碼往下的所有内容(注意不要删掉類自己的括号):

現在,自從加入這個類以後Xcode顯示的那幾條警告(warning)也應該消失了。

運作App,檢查使用靜态表項的新頁面。完全沒有寫代碼,其實剛才還删了一段代碼!

還 要了解一點:靜态表項隻在`UITableViewController`中有效,雖然Interface Builder允許你在正常`UIViewController`中的表視圖對象裡添加靜态表項,運作時不會發揮作用,原因是 `UITableViewController`中額外實作了一些用來處理靜态表項資料源的操作。在項目中誤用的話Xcode甚至會拒絕編譯,輸出報錯信 息:“Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances”。

另一方面,原型表項在正常視圖内的表視圖中可以正常工作,但在nib中就沒戲了。目前來講,使用原型表項或靜态表項就必須使用Storyboard。

你也有可能想在一個表視圖中混合使用靜态表項和正常的動态表項,很遺憾的是目前的SDK對此支援欠佳。如果你的App有這種需求,請參考蘋果開發者官方論壇上的​​相關文章​​尋求可行方案。

注:如果建構的頁面上包含的靜态表項多到無法在可視範圍内全部展示,你可以在Interface Builder中直接利用滾動手勢檢視,這個功能可能不容易發現,但确實管用。

不過總的來說該寫代碼的地方隻能靠代碼,甚至靜态表項的表視圖也是如此。前面在把文本字段拖進第一個表項的時候,你可能發現尺寸不大合适,文本字段周圍有一點白邊,而且使用者看不到文本字段的實際範圍,如果正好點在邊框上,沒有彈出鍵盤,使用者會感到困惑。

為避免這種情況,你應該讓那一行任意位置接受的點選都可以喚出鍵盤。要這樣做很容易,打開PlayerDetailsViewController.swift并如下添加

代碼的意思是如果使用者點按第一個表項,App應該激活相應文本字段。該分段隻有一個表項,你隻需使用分段的索引。設文本字段為第一響應者會自動喚出鍵盤。這隻是一小處使用者體驗優化,但就是這樣一個小細節可以給使用者省去一點煩惱。

小訣竅:添加delegate委托方法或重寫視圖控制器方法時,直接輸入方法名開頭的幾個字母(前面不加func),即可在自動補全清單中選擇正确的方法。

另外,還應該在Storyboard的屬性檢查器中把相應表項的Selection Style設為None(原本是Default),否則使用者點按文本字段周圍的邊框時該行會高亮。

Swift Swift語言Storyboard教程:第二部

好啦,添加玩家頁面設計完成。現在我們要實作功能。

為添加玩家頁面實作功能

現在先不管Game這行,隻輸入玩家名稱。

當使用者點選Cancel按鈕時,頁面關閉,使用者剛剛輸入的資料随之廢棄。這部分功能直接用回退轉場已經實作好了。

而當使用者點選Done時,你應該建立一個新的Player對象,參照使用者輸入填充屬性後更新玩家清單。

轉場即将發生時,`prepareForSegue(:sender:)`會被調用。你可以重寫這個方法,在退出視圖之前将資料儲存到一個新的Player對象中。

注:不要擅自調用`prepareForSegue`方法,這是UIKit通知你一個轉場剛剛被觸發的消息。

在PlayerDetailsViewController.swift中,先在類上添加一條屬性:

這條語句并不會将屬性執行個體化,但其中的感歎号把該變量定義為隐式解包可選量(implicitly unwrapped optional),意思是該變量必須被執行個體化,而且你确定它在被使用前一定有值。

接下來在PlayerDetailsViewController.swift中添加以下方法:

prepareForSegue(_:sender:)`方法判斷轉場的辨別符是否為`SavePlayerDetail`,當且僅 當判定結果為真時,建立一個新的Player執行個體,其中game和rating均取預設值。如果此時運作,App會崩潰,因為不存在辨別符 `SavePlayerDetail`,player不會被執行個體化,結合前面的隐式解包可選量定義,引發運作時錯誤。

小提示:如果App出現詭異的崩潰問題,而且代碼看起來似乎并無邏輯錯誤,那麼可能是在代碼中删除過對象或修改過對象名,以緻Storyboard引用對象出錯。

在Main.storyboard中,在文檔大綱裡找到Add Player場景,選擇連接配接到`savePlayerDetail`這個action的回退轉場,将其辨別符改為`SavePlayerDetail`:

Swift Swift語言Storyboard教程:第二部

然後選擇連接配接到`cancelToPlayersViewController`的回退轉場,将其辨別符改為`CancelPlayerDetail`。以供`prepareForSegue(_:sender:)`方法判斷辨別符。

轉到PlayersViewController類,如下修改回退轉場方法`savePlayerDetail(segue:)`:

這會通過傳入方法的轉場引用擷取一個指向`PlayerDetailsViewController`的引用,并借此向資料源中使用的Player數組添加新的Player對象,然後通知表視圖在末尾新增了一行,因為表視圖和資料源應當保持同步。

你可能會直接調用`tableView.reloadData()`,但還是為新行插入的操作加入動畫效果比較好。`UITableViewRowAnimation.Automatic`會以插入新行的位置自動選用合适的動畫,十分友善。

試試看,現在應該可以向清單中加入新玩家了!

Swift Swift語言Storyboard教程:第二部

性能

現 在Storyboard已經有好幾個視圖控制器了,你或許會擔心性能問題,不過一次載入整個Storyboard并不是什麼苦活,Storyboard不 會立即執行個體化所有的視圖控制器,立即載入的隻有初始視圖控制器。而由于這裡的初始視圖控制器是一個分頁欄控制器,包含的兩個視圖控制器也會被載入(第一個 分頁标簽的Players場景和第二個分頁标簽的場景)。

其他視圖控制器隻有在轉場過去的時候才會被執行個體化。而當關閉視圖控制器的時候,它們會立即被釋放,是以記憶體中隻有活躍使用的視圖控制器,就好像分别使用nib一樣。

實踐是檢驗真理的唯一标準,在PlayerDetailsViewController類中添加構造器(initializer)和析構器(deinitializer):

你剛剛重寫了`init(coder:)`和`deinit`方法,讓它們向Xcode調試面闆輸出資訊。現在運作App,打開添加玩家頁面,你會發現視圖控制器隻有在被打開的時候才會配置設定。

關閉添加玩家頁面的時候,無論是點選Cancel還是Done都會看到deinit析構器的`println()`輸出。如果再次打開這個頁面,你還會看到`init(coder:)`的輸出,這樣你應該相信這個事實了:視圖控制器是按需加載的,就像手動載入nib一樣。

注: 如果你以前用過nib,那麼你應該會很熟悉構造器`init(coder:)`,這部分機制延續到了Storyboard中:使用的方法依然是 `init(coder:)`,`awakeFromNib()`和`viewDidLoad()`。Storyboard可以看成附帶了過渡資訊和關聯 資訊的一系列nib的集合,而Storyboard内的視圖和視圖控制器使用與nib相同的方式編碼并解析。

遊戲選擇頁面

在添加玩家頁面中點選Game行應該打開一個新頁面并讓使用者從清單中選擇一個遊戲,這意味着下一步要加入另外一個表視圖控制器,不過這次的頁面不是模态顯示,而是壓入導航棧。

向 Storyboard中拖入一個新的表視圖控制器,在添加玩家頁面中選擇Game表項(確定選中的是整個表項,而不是其中的label),然後按住 control拖到建立的表視圖控制器,在兩者之間建立轉場。在彈出的選單中選擇轉場類型為Push,然後在屬性檢查器中把轉場的Identifier标 識符設為PickGame。

輕按兩下導航欄,将新場景命名為Choose Game。設原型表項的Style為Basic(基本),設重用辨別符為GameCell,如圖:

Swift Swift語言Storyboard教程:第二部

在 項目中使用Cocoa Touch Class模闆建立一個Swift檔案,命名為GamePickerViewController,繼承UITableViewController。回 到Storyboard中将遊戲選擇頁面的Custom Class設為`GamePickerViewController`。

現在為新頁面添加資料。在GamePickerViewController.swift中,在開頭添加games屬性,然後重寫viewDidLoad函數,像這樣:

你剛剛新增了一個叫做`games`的字元串數組,并在`viewDidLoad()`中用寫定的内容填充數組。

然後如下替換資料源方法:

上述代碼将`games`數組設為資料源并替換表項的textLabel中的字元串值。

隻要資料源準備就緒就應該能正常工作。運作App,點選Game行,新的遊戲選擇頁面會滑入螢幕。現在點選各項不會有什麼效果,但由于該頁面呈現在導航棧上,你可以直接點選傳回按鈕,傳回原來的添加玩家頁面。

Swift Swift語言Storyboard教程:第二部

不用寫代碼就可以喚出新頁面,是不是很贊?隻要按住control從靜态表項拖到新場景,寫的代碼隻有填充表視圖的内容,而且一般來講比原地設計好的清單要靈活些(因為games數組更友善修改)。

當然新頁面要傳回資料才有用,為此你要添加一個新的回退轉場。

在GamePickerViewController類的上面添加持有選中的遊戲的名稱和索引的屬性:

然後修改`cellForRowAtIndexPath:`:

這會在目前所選遊戲對應的表項附上選中标記(對号),這對使用者體驗來說不可或缺。

接着添加`tableview(tableview:didSelectRowAtIndexPath:)`方法:

這段代碼首先會取消選擇剛剛點選的行,外觀會從灰色高亮變回正常的白色,然後移除對号,并在剛剛點選的行上附加選中标記。

運作App,測試是否正常。點選一個遊戲名,相應行會附上選中标記,點選另一個遊戲名,選中标記也随之移動。

Swift Swift語言Storyboard教程:第二部

按要求來說點選某行之後應該關閉該頁面,不過現在并沒有自動傳回,因為尚未綁定回退轉場。

在PlayerDetailsViewController.swift的類上面添加一個持有被選遊戲的屬性,以便之後在Player對象中儲存。令其預設值為"Chess",這樣一來新玩家總會有一個標明的遊戲。

同樣在該檔案中改寫`viewDidLoad()`以在靜态表項中遊戲名稱:

添加回退轉場方法:

上述代碼會在使用者從選擇遊戲場景選中一個遊戲後執行。該方法按照選中的遊戲更新頁面上的label和game屬性,然後将GamePickerViewController彈出導航棧。

在Main.storyboard中按住control從表項拖到Exit出口對象,然後從彈出清單中選擇`selectedGame:`。

Swift Swift語言Storyboard教程:第二部

設該回退轉場辨別符為SaveSelectedGame。

運作App試試看,建立新玩家,點選Game行并選擇一個遊戲。

Swift Swift語言Storyboard教程:第二部

不 幸的是,這個回退轉場方法是在`tableView(_:didSelectRowAtIndexPath:)`方法前執行的,是以 `selectedGameIndex`并未及時更新。幸運的是你可以重寫`prepareForSegue(_:sender:)`方法,在轉場之前完 成更新操作。

在GamePickerViewController中添加`prepareForSegue(segue:)`方法:

`prepareForSegue(_:sender:)`的sender參數是引發轉場的對象,在這裡對應選中的遊戲表項,是以你可以利用表項的indexPath來在games數組中确定選中的遊戲并在轉場發生之前更新`selectedGame`。

現在運作App,選擇遊戲後玩家的遊戲資訊會随之更新了。

Swift Swift語言Storyboard教程:第二部

接下來改寫PlayerDetailsViewController的prepareForSegue方法來傳回選中的遊戲,而不是寫定的"Chess"。這樣一來,完成添加玩家的操作後,Players場景中會顯示玩家實際選擇的遊戲。

在PlayerDetailsViewController.swift中如下改寫`prepareForSegue(_:sender:)`方法:

完成添加玩家頁面并點選Done後,玩家清單會更新正确的遊戲資訊。

還有一點,當你選擇一個遊戲,傳回添加玩家頁面,然後嘗試重新選擇遊戲的時候,之前標明的遊戲應該顯示選中标記。解決方法是在轉場時把PlayerDetailsViewController中儲存的選中的遊戲傳給GamePickerViewController。

還是在PlayerDetailsViewController.swift中,于`prepareForSegue(segue:,sender:)`方法的末尾添加以下代碼:

注意:現在有兩條檢查`segue.identifier`的 `if`語句。SavePlayerDetail是傳回玩家清單的回退轉場,PickGame是前往遊戲選擇頁面的入棧轉場。添加的代碼會在 GamePickerViewController的視圖加載之前更新其中的`selectedGame`。

打開GamePickerViewController.swift并在`viewDidLoad()`末尾添加以下代碼:

這兩行代碼擷取從 PlayerDetailsViewController傳進的selectedGame并将其轉換成正确的索引。`find()`函數會在games數 組中查找比對selectedGame的String,然後傳回比對元素的索引,指派給selectedGameIndex,這個索引用來在對應表項上設 置選中标記。

好。現在選擇遊戲頁面功能實作完成!

Swift Swift語言Storyboard教程:第二部

何去何從?

這是整個教程的​​示例項目​​,包含上述所有源代碼。

可喜可賀,現在你已經了解Storyboard編輯器的基本用法,能夠建立包含多個視圖控制器并能通過轉場在場景之間切換的App!在一處集中管理多個視圖控制器和互相的關聯,讓整體把握App的樣子更加容易。

你也看到了自定義表視圖和表項有多麼容易。有了靜态表項,不用實作所有的資料源方法也可以建構一些界面。

如果想深入了解Storyboard,請參閱我們的書籍​​iOS 8教程​​,其中涵蓋了最新的通用Storyboard(Universal Storyboard)。