[IOS] Storyboard全解析
- 原文地址:http://iaiai.iteye.com/blog/1493956
(Storyboard)是一個能夠節省你很多設計手機App界面時間的新特性,下面,為了簡明的說明Storyboard的效果,我貼上本教程所完成的Storyboard的截圖:
現在,你就可以清楚的看到這個應用究竟是干些什么的,也可以清楚的看到其中的各種關系,這就是Storyboard的強大之處了。如果你要制作一個頁面很多很復雜的App,Storyboard可以幫助你解決寫很多重復的跳轉方法的麻煩,節省很多時間,以便你能夠完全的專注於核心功能的實現上。
開始
首先啟動Xcode,新建一個工程,我們在這里使用Single View App Template,這個模板會提供一個類和一個Storyboard,免去我們自己創建的麻煩。
創建完成之后,Xcode的界面大概是這樣的:
這個新的工程由兩個類:AppDelegate和ViewController以及一個Storyboard組成(如果你選擇了兩個設備會有兩個Storyboard),注意這個項目沒有xib文件,讓我們首先看看Storyboard是什么樣的,雙擊Storyboard打開他:
Storyboard的樣子和工作方式都和Interface Builder(以下簡稱為IB)像極了,你可以從左下方的控件庫中拖動控件到你的View之中並且組織他們的排放順序,唯一不同的地方就是,Storyboard不止是包含一個視圖控件,而是所有的視圖控件以及他們之間的關系。
Storyboard對一個視圖的官方術語是一個場景,但是一個場景其實就是一個ViewController,在iPhone中一次只能夠展示一個場景,而在iPad中一次可以展示多個場景,比如Mail應用程序。
通過嘗試添加一些控件,你可以感受一下Storyboard的工作方式。
這個是數據顯示器,顯示所有場景及其控件的結構。
在IB中,這個位置顯示的是你的NIB文件中的文件,而在Storyboard中這里顯示的是ViewController,目前這里只有一個ViewController,我們接下來可能會增加一些。
這是一個文檔管理器的縮小版,叫做dock。
Dock展示場景中第一級的控件,每個場景至少有一個ViewController和一個FirstReponder,但是也可以有其他的控件,Dock還用來簡單的連接控件,如果你需要向ViewController傳遞一個關系時,只需要將其按住Ctrl鍵拖到ViewController上就可以了。
Note:你大概不會太長使用FirstResponder,因為它只是一個代理控件,代表着當前你所使用的控件。
現在運行這個應用,他會向我們設計的界面一樣。
如果你以前制作過NIB型的應用的話,你也許回去尋找MainWindow.xib ,這個文件包括所有的ViewController,Appdelegate等等,但是在Storyboard中這個特性已經被廢止了。
那么,沒有這個文件,應用從那里起始呢?
讓我們打開AppDelegate文件,看看那上面是怎么說的:
如果要使用Storyboard特性,那么AppDelegate必須繼承自UIResponder類, 之前則是繼承自NSObject類的,而且必須有一個不是UIOutlet類的Window屬性聲明才可以。
如果你再去看AppDelegate的執行文件,里面大概什么都沒有,甚至連 application:didFinishLaunchingWithOptions: 也只是返回了一個 YES,而之前,這里則需聲明一個ViewController並且將他設置成起始頁面,但是現在這些都沒有了。
秘密就在info.plist文件中, 打開Ratings-Info.plist (在 Supporting Files group里) 你就會看到這些:
在NIB為UI的應用里,info.plist文件中有一個鍵兼做NSMainNibFile,或者叫做Main nib file base name,他用來指示UIApplication載入MainWindow.xib,並且將他與應用鏈接起來,而現在這個鍵值消失了。
而Storyboard應用則利用 UIMainStoryboardFile,或者 “Main storyboard file base name” 鍵值來表示當App初始化時的Storyboard名稱,當程序運行時,UIApplication會使用MainStoryboard.sotryboard作為第一加載項,並且將他的UIWindow展示在屏幕上,不需要任何編程工作。
在項目總結面板上,你也可以看到並且編輯這些信息:
如果你還想設置nib文件的話,另外有地方去設置的。
為了完成這個實驗性的小程序,我們打開main.m,加入
之前是UIApplicationMain()的函數現在是空的, 變成了 NSStringFromClass([AppDelegate class]).
與之前使用MainWindow.xib的一個最大的不同是:現在app delegate已經不是Storyboard的一部分了,這是因為app delegate不再從nib文件中,而侍從Storyboard中加載了,我們必須告訴 UIApplicationMain 我們的app delegate類的名字是什么,否則他將無法找到。
制作一個Tab類型的應用
本教程中的Rating App擁有兩個Tab,在Storyboard中,很輕松就能夠做出一個Tab視圖。
回到MainStoryboard.storyboard中,直接從左邊的Library拖進來一個TabViewController就可以了。
新的Tab Bar Controller附帶了兩個View controller,分別作為Tab的視圖使用,UITabBarController被稱為包含視圖,因為他包含這其他一些View,其他常見的包含視圖還有那vi嘎提鷗鳥 Controller和SplitView Controller。
在iOS 5中,你還可以自己寫一個自定義的Controller,這在以前是做不到的。
包含關系在Storyboard中用一下這種箭頭表示。
拉一個Label控件到第一個子試圖中,命名為“First Tab”,再在第二個子視圖中添加一個Label,命名為“Second Tab”。
注意:當屏幕的縮放大於100%時,你無法在單個場景中添加控件。
選中Tab Bar Controller,進入屬性檢查器,選中“作為起始場景”,如下圖:
現在那個沒有頭的虛虛的小箭頭指向了Tab Bar Controller,說明他是起始場景。
這意味着,當你啟動這個應用的時候,UIApplication將會將這個場景作為應用的主屏幕。
Storyboard一定要有一個場景是起始場景才行。
現在運行試試吧
code專門為創造這種Tab Bar的應用准備了一個模板,我們也可以使用他,但是自己有能力不用模板自己做一個Tab Bar也是不錯的事。
如果你添加了多於五個子視圖到一個TabBarcontroller的話,並不會創造五個Tab,第四個tab會自動變成More標簽,不錯吧
制作一個表格視圖
目前連接到Tab bar Controller的視圖都是普通的View Controller,現在,我要用一個TableViewController來代替其中的一個ViewController。
單擊第一個視圖並刪除,從Library中拖出一個TableViewController。
在選中這個TableViewController的前提下,從Library中拖出一個NavController,將會直接附着在上面。
當然也可以調換順序,我完全沒意見。
由於NavController和TabBarController一樣也是一個包含控制器視圖,所以他也必須包含另一個視圖,你可以看到同樣的箭頭連接者這兩個View。
請注意所有嵌套在NavController下的View都會有一個Navigation Bar,你無法移除它,因為他是一個虛擬的Bar。
如果你檢視屬性檢測器,你就會發現所有bar的屬性都在一起:
“Inferred”是Storyboard中的默認設置,他意味着繼承的關系,但是你也可以改變他。但是請注意這些設置都是為了讓你更好的進行設計和這樣設置的,隨意修改默認設置會帶來不可遇見的后果,施主自重。
現在讓我們把這個新的場景連接到Tab Bar Controller中,按住Ctrl拖動,或者右鍵。
當你放手的時候,一個提示框會出現。
當然是選第一個了,Relationship – viewControllers ,這將自動創建兩個場景之間的關系。
直接拖動就可以改變Tab Item的順序,同時也會改變顯示Tab的順序,放在最左邊的Tab會第一個顯示。
現在運行試試看吧
在我們在這個應用中加入任何實質性的功能之前,我們先來清理一下Storyboard,你不需要改變TabBarController中的任何內容而只需要改變他的子視圖就可以了。
每當你連接一個新的視圖到TabBarController中的時候,他就會自動增加一個Tab Item,你可以使用他的子視圖來修改該Item的圖片和名稱。
在NavController中選中Tab Item並且在屬性編輯其中將其修改為Player。
將第二個Tab Item命名為“Gesture”
我們接下來把自定義的圖片加入到這些item中, 源碼 中包含一個名為“Image”的文件夾,在那里你可以找到我們用到的資源。
接下來,將NavController的title改為Player,也可以使用代碼··
運行看一看,難以置信吧,你到現在也沒寫一條代碼。
原型表格單元
你也許已經注意到了,自從我們加入了Table View Controller之后,Xcode便會現實下面這樣一條警告。
這條警告是:“Unsupported Configuration: Prototype table cells must have reuse identifiers”意思是,原型表格單元必須有一個身份證(意譯啦)
原型單元格是另一個Storyboard的好特性之一。在之前,如果你想要自定義一個Table Cell,那么你就不得不用代碼來實現,要么就要單獨創建一個Nib文件來表示單元格內容,現在你也可以這樣做,不過原型單元格可以幫你把這一過程大大的簡化,你現在可以直接在Storyboard設計器中完成這一過程。
Table View現在默認的會帶有一個空白的原型單元格,選中他,在屬性控制器中將他的Style改為subtitle,這樣的話,每一格就會有兩行字。
將附件設置為Disclosure Indicator並且將這個原型單元格的Reuse Identifier 設置喂“PlayerCell”,這將會解決Xcode所警告的問題。
試着運行一個,發現什么都沒變,這並不奇怪,因為我們還沒有給這個表格設置一個數據來源(DataSource),用以顯示。
新建一個文件,使用UIViewContoller模板,命名為 PlayersViewController ,設置喂UITableViewController的子類,不要勾選建立XIB文件。
回到Storyboard編輯器,選擇Table View Controller,在身份控制器中,把他的類設置為PlayerViewController,這對於把Storyboard中的場景和你自定義的子類掛鈎是十分重要的。要是不這么做,你的子類根本沒用。
現在起,當你運行這個應用時,table view controller其實是PlayersViewContoller的一個實例。
在 PlayersViewController.h 中聲明一個MutableArray(可變數組)
這個數組將會包含我們的應用的主要數據模型。我們現在加一些東西到這個數組之中,新建一個使用Obj-c模板的文件,命名為player,設置喂NSObject的子類,這將會作為數組的數據容器。
編寫Player.h如下:
編寫Player.m如下:
這里沒有什么復雜的,Player類只是一個容器罷了,包含三個內容:選手的名字、項目和他的評級。
接下來我們在App Delegate中聲明數組和一些Player對象,並把他們分配給PlayerViewController的players屬性。
在AppDelegate.m中,分別引入(import)Player和PlayerViewController這兩個類,之后新增一個名叫players的可變數組。
修改didFinishLaunchingWithOptions方法如下:
這將會創造一些Player對象並把他們加到數組中去。之后在加入:
咦,這是什么?目前的情況是:我們希望能夠將players數組連接到PlayersViewController的players屬性之中以便讓這個VC能夠用做數據來源。但是app delegate根本不了解PlayerViewController究竟是什么,他將需要在storyboard中尋找它。
這是一個我不是很喜歡storyboard特性,在IB中,你在MainWindow.xib中總是會有一個指向App delegate的選項,在那里你可以在頂級的ViewController中向Appdelegate設置輸出口,但是在Storyboard中目前這還不可能,目前只能通過代碼來做這樣的事情。
我們知道storyboard的起始場景是Tab Bar Controller,所以我們可以直接到這個場景的第一個子場景來設置數據源。
PlayersViewController 在一個NavController的框架之中,所以我們先看一看UINavigationController類:
然后詢問它的根試圖控制器,哪一個是我們要找的PlayersViewController:
但是,UIViewController根本就沒有一個rootViewController屬性,所以我們不能把數組加入進去,他又一個topViewController但是指向最上層的視圖,與我們這里的意圖沒有關系。
現在我們有了一個裝在了players物體合集的數組,我們繼續為PlayersViewController設置數據源。
打開PlayersViewController.m,加入以下數據源方法:
真正起作用的代碼在cellForRowAtIndexPath方法里,默認的模板是如下這樣的:
無疑這就是以前設置一個表格視圖的方法,不過現在已經革新了,把這些代碼修改如下:
這看上去簡單多了,為了新建單元格,你只需使用如下代碼:
如果沒有現存的單元格可以回收,程序會自動創造一個原型單元格的復制品之后返回給你,你只需要提供你之前在Storyboard編輯視圖中設置的身份證就可以的,在這里就是“PlayerCell”,如果不設置這個,這個程序就無法工作。
由於這個類對於Player容器目前一無所知,所以我們需要在文件的開頭加入一個引入來源
記得要創建synthesize語句哦親
現在運行應用,會看到Table里有着players容器。
請注意:我們這里只使用一種單元格原型,如果你需要使用不同類型的單元格的話,只需要在storyboard中另外加入一個單元格原型就可以了,不過不要忘記給他們指派不同的身份證。
設計自定義的原型單元格
對於很多應用來說,使用默認的單元格風格就OK了,但是我偏偏要在每一個單元格的右邊加上一個一個圖片來表示選手的評級,但是添加圖片對於默認類型的單元格來說並不支持,我們需要自定義一個設計。
讓我們轉回MainStoryboard.storyboard,選中table view中的prototype cell,把它的Style attribute改為Custom,所有默認的標簽都會消失。
首先把單元格變得更高一些,你可以直接拉它,也可以在大小控制器中修改數字,我在這里使用55點的高度。
從 Objects Library中拖出兩個標簽物體,按照之前的樣式安插到單元格里,記得設置label的Highlighted顏色為白色,那樣的話當單元格被選中的時候會看起來更好看一些。
之后添加一個Image View對象,將它放置在單元格的右邊,設置他的寬度為81點,高度並不重要,在屬性檢查器中設置模式為置中。
我把標簽設置為210點長以確保他不會和ImageView重合,最后整體的設計會看起來象下面這樣:
由於這是一個自定義的單元格,所以我們不能夠使用UITableView默認的textLabel和detailLabel來設置數據,這些屬性也不再指向我們的單元格了,我們使用標簽(tags)來指定標簽。
將Name標簽的tag設置為100,Game的設置喂101,image的設置喂102,在屬性檢查器里設置哦親。
之后打開 PlayersViewController.m ,在PlayersViewcontroller中將cellForRowatIndexPath修改為:
這里是用了一個新的方法,叫做ImageRating,在 cellForRowAtIndexPath方法之前加入這個方法:
這就完成了,運行看看:
這和我們想象的結果並不是很符合,我們修改了原型單元格的屬性和高度,但是table view卻沒有考慮進去,有兩種方法可以修復它,我們可以改變table view的行高或者加入 heightForRowAtIndexPath 方法來修改,地一種方法更簡單,我們就用他。
注意:在一下兩種情況下,你應該使用 heightForRowAtIndexPath 方法:一是,你不能預先知道你的單元格的高度,二是不同的單元格會有不同的高度。
回到MainStoryboard.storyboard,在大小檢查器中將高度設置為55:
通過這種方式的話,如果之前你是使用拖動而不是鍵入數值的方式改變高度的屬性的話,則table view的數值也會自動改變。
現在運行看看,好多了吧
為原型單元格設置子類
我們的表格視圖已經相當像模像樣了,但是我並不是很喜歡使用tag來訪問label,要是我們能夠把這些lable連接到輸出口,之后在回應屬性中使用他們,該多好,而且不出所料,我們可以這樣做。
使用 Objective-C class模板新建一個文件,命名為PlayerCell,繼承UITableViewCell。
修改PlayerCell.h
修改PlayerCell.m
這個類本身並不其很大的作用,只是為nameLabel、gameLabel和ratingImageView聲明了屬性。
回到MainStoryboard.storyboard選中原型單元格,將他的class屬性修改為“PlayerCell”,現在當你向table view請求dequeueReusableCellWithIdentifier,他會返回一個PlayerCell實例而不是一個普通的UITableViewCell實例。
請注意我將這個類和reuse Indetifier的名字命名的一樣,只是營衛我喜歡這樣哦親,這兩個之間其實沒啥關系。
現在你可以將標簽和image view連接到輸出口去了,選中或者將他從鏈接檢查器拖動到table view cell。
請注意:要把這個control連接到table view cell而不是view controller哦親,別選錯了。
現在我們把一切都鏈接好了,只需要加入數據源的代碼就可以了。
我們現在將接收到 dequeueReusableCellWithIdentifier 的控件指派到PlayerCell,只需要簡單的使用已經鏈接labels和image view到設置好的屬性上就可以了,這會讓這個設計看上去更加好控制,更加簡明。
當然,在PlayerCell前要引入資源:
試着運行,你會發現其實什么都沒有變化,可是我們都知道,內部已經有了變化。
在這相同的場景下面,我們可是在使用子類呢。
這里還有一些設計小竅門:第一點:一定要設置標簽被選中時的顏色。
第二點,確保你加入單元格的字符大小是可以變化的,這樣,當單元格大小變化時,他的內容的大小也會跟着變化,比如說:
在PlayersViewController.m中加入如下方法:
這個方法加入好了之后,用手指輕掃一行單元格,會出現一個刪除鍵,試試看
Delete按鈕出現在右邊,遮住了一部分評級圖片,怎么解決呢?
打開MainStoryBoard.storyboard,選中table view cell中的image view,在大小檢查器中修改Autosizing屬性,是它能夠跟隨上級view的邊緣。
為labels設置同樣的屬性。
加入了這些變動之后,刪除按鈕如我們意料的出現了:
其實,最好的做法是讓這些星星在出現delete按鈕的時候消失,不過這只是一個練習,不要太較真哦親

現在,你就可以清楚的看到這個應用究竟是干些什么的,也可以清楚的看到其中的各種關系,這就是Storyboard的強大之處了。如果你要制作一個頁面很多很復雜的App,Storyboard可以幫助你解決寫很多重復的跳轉方法的麻煩,節省很多時間,以便你能夠完全的專注於核心功能的實現上。
開始
首先啟動Xcode,新建一個工程,我們在這里使用Single View App Template,這個模板會提供一個類和一個Storyboard,免去我們自己創建的麻煩。

創建完成之后,Xcode的界面大概是這樣的:

這個新的工程由兩個類:AppDelegate和ViewController以及一個Storyboard組成(如果你選擇了兩個設備會有兩個Storyboard),注意這個項目沒有xib文件,讓我們首先看看Storyboard是什么樣的,雙擊Storyboard打開他:

Storyboard的樣子和工作方式都和Interface Builder(以下簡稱為IB)像極了,你可以從左下方的控件庫中拖動控件到你的View之中並且組織他們的排放順序,唯一不同的地方就是,Storyboard不止是包含一個視圖控件,而是所有的視圖控件以及他們之間的關系。
Storyboard對一個視圖的官方術語是一個場景,但是一個場景其實就是一個ViewController,在iPhone中一次只能夠展示一個場景,而在iPad中一次可以展示多個場景,比如Mail應用程序。
通過嘗試添加一些控件,你可以感受一下Storyboard的工作方式。

這個是數據顯示器,顯示所有場景及其控件的結構。

在IB中,這個位置顯示的是你的NIB文件中的文件,而在Storyboard中這里顯示的是ViewController,目前這里只有一個ViewController,我們接下來可能會增加一些。
這是一個文檔管理器的縮小版,叫做dock。

Dock展示場景中第一級的控件,每個場景至少有一個ViewController和一個FirstReponder,但是也可以有其他的控件,Dock還用來簡單的連接控件,如果你需要向ViewController傳遞一個關系時,只需要將其按住Ctrl鍵拖到ViewController上就可以了。
Note:你大概不會太長使用FirstResponder,因為它只是一個代理控件,代表着當前你所使用的控件。
現在運行這個應用,他會向我們設計的界面一樣。

如果你以前制作過NIB型的應用的話,你也許回去尋找MainWindow.xib ,這個文件包括所有的ViewController,Appdelegate等等,但是在Storyboard中這個特性已經被廢止了。

那么,沒有這個文件,應用從那里起始呢?
讓我們打開AppDelegate文件,看看那上面是怎么說的:
- #import <UIKit/UIKit.h>
- @interface AppDelegate : UIResponder <UIApplicationDelegate>
- @property (strong, nonatomic) UIWindow *window;
- @end
如果要使用Storyboard特性,那么AppDelegate必須繼承自UIResponder類, 之前則是繼承自NSObject類的,而且必須有一個不是UIOutlet類的Window屬性聲明才可以。
如果你再去看AppDelegate的執行文件,里面大概什么都沒有,甚至連 application:didFinishLaunchingWithOptions: 也只是返回了一個 YES,而之前,這里則需聲明一個ViewController並且將他設置成起始頁面,但是現在這些都沒有了。
秘密就在info.plist文件中, 打開Ratings-Info.plist (在 Supporting Files group里) 你就會看到這些:

在NIB為UI的應用里,info.plist文件中有一個鍵兼做NSMainNibFile,或者叫做Main nib file base name,他用來指示UIApplication載入MainWindow.xib,並且將他與應用鏈接起來,而現在這個鍵值消失了。
而Storyboard應用則利用 UIMainStoryboardFile,或者 “Main storyboard file base name” 鍵值來表示當App初始化時的Storyboard名稱,當程序運行時,UIApplication會使用MainStoryboard.sotryboard作為第一加載項,並且將他的UIWindow展示在屏幕上,不需要任何編程工作。
在項目總結面板上,你也可以看到並且編輯這些信息:

如果你還想設置nib文件的話,另外有地方去設置的。
為了完成這個實驗性的小程序,我們打開main.m,加入
- #import <UIKit/UIKit.h>
- #import "AppDelegate.h"
- int main(int argc, char *argv[])
- {
- @autoreleasepool {
- return UIApplicationMain(argc, argv, nil,
- NSStringFromClass([AppDelegate class]));
- }
- }
之前是UIApplicationMain()的函數現在是空的, 變成了 NSStringFromClass([AppDelegate class]).
與之前使用MainWindow.xib的一個最大的不同是:現在app delegate已經不是Storyboard的一部分了,這是因為app delegate不再從nib文件中,而侍從Storyboard中加載了,我們必須告訴 UIApplicationMain 我們的app delegate類的名字是什么,否則他將無法找到。
制作一個Tab類型的應用
本教程中的Rating App擁有兩個Tab,在Storyboard中,很輕松就能夠做出一個Tab視圖。
回到MainStoryboard.storyboard中,直接從左邊的Library拖進來一個TabViewController就可以了。

新的Tab Bar Controller附帶了兩個View controller,分別作為Tab的視圖使用,UITabBarController被稱為包含視圖,因為他包含這其他一些View,其他常見的包含視圖還有那vi嘎提鷗鳥 Controller和SplitView Controller。
在iOS 5中,你還可以自己寫一個自定義的Controller,這在以前是做不到的。
包含關系在Storyboard中用一下這種箭頭表示。

拉一個Label控件到第一個子試圖中,命名為“First Tab”,再在第二個子視圖中添加一個Label,命名為“Second Tab”。
注意:當屏幕的縮放大於100%時,你無法在單個場景中添加控件。
選中Tab Bar Controller,進入屬性檢查器,選中“作為起始場景”,如下圖:

現在那個沒有頭的虛虛的小箭頭指向了Tab Bar Controller,說明他是起始場景。

這意味着,當你啟動這個應用的時候,UIApplication將會將這個場景作為應用的主屏幕。
Storyboard一定要有一個場景是起始場景才行。
現在運行試試吧

code專門為創造這種Tab Bar的應用准備了一個模板,我們也可以使用他,但是自己有能力不用模板自己做一個Tab Bar也是不錯的事。
如果你添加了多於五個子視圖到一個TabBarcontroller的話,並不會創造五個Tab,第四個tab會自動變成More標簽,不錯吧
制作一個表格視圖
目前連接到Tab bar Controller的視圖都是普通的View Controller,現在,我要用一個TableViewController來代替其中的一個ViewController。
單擊第一個視圖並刪除,從Library中拖出一個TableViewController。

在選中這個TableViewController的前提下,從Library中拖出一個NavController,將會直接附着在上面。

當然也可以調換順序,我完全沒意見。
由於NavController和TabBarController一樣也是一個包含控制器視圖,所以他也必須包含另一個視圖,你可以看到同樣的箭頭連接者這兩個View。

請注意所有嵌套在NavController下的View都會有一個Navigation Bar,你無法移除它,因為他是一個虛擬的Bar。
如果你檢視屬性檢測器,你就會發現所有bar的屬性都在一起:

“Inferred”是Storyboard中的默認設置,他意味着繼承的關系,但是你也可以改變他。但是請注意這些設置都是為了讓你更好的進行設計和這樣設置的,隨意修改默認設置會帶來不可遇見的后果,施主自重。
現在讓我們把這個新的場景連接到Tab Bar Controller中,按住Ctrl拖動,或者右鍵。

當你放手的時候,一個提示框會出現。

當然是選第一個了,Relationship – viewControllers ,這將自動創建兩個場景之間的關系。


直接拖動就可以改變Tab Item的順序,同時也會改變顯示Tab的順序,放在最左邊的Tab會第一個顯示。
現在運行試試看吧

在我們在這個應用中加入任何實質性的功能之前,我們先來清理一下Storyboard,你不需要改變TabBarController中的任何內容而只需要改變他的子視圖就可以了。
每當你連接一個新的視圖到TabBarController中的時候,他就會自動增加一個Tab Item,你可以使用他的子視圖來修改該Item的圖片和名稱。
在NavController中選中Tab Item並且在屬性編輯其中將其修改為Player。

將第二個Tab Item命名為“Gesture”
我們接下來把自定義的圖片加入到這些item中, 源碼 中包含一個名為“Image”的文件夾,在那里你可以找到我們用到的資源。
接下來,將NavController的title改為Player,也可以使用代碼··

運行看一看,難以置信吧,你到現在也沒寫一條代碼。

原型表格單元
你也許已經注意到了,自從我們加入了Table View Controller之后,Xcode便會現實下面這樣一條警告。

這條警告是:“Unsupported Configuration: Prototype table cells must have reuse identifiers”意思是,原型表格單元必須有一個身份證(意譯啦)
原型單元格是另一個Storyboard的好特性之一。在之前,如果你想要自定義一個Table Cell,那么你就不得不用代碼來實現,要么就要單獨創建一個Nib文件來表示單元格內容,現在你也可以這樣做,不過原型單元格可以幫你把這一過程大大的簡化,你現在可以直接在Storyboard設計器中完成這一過程。
Table View現在默認的會帶有一個空白的原型單元格,選中他,在屬性控制器中將他的Style改為subtitle,這樣的話,每一格就會有兩行字。


將附件設置為Disclosure Indicator並且將這個原型單元格的Reuse Identifier 設置喂“PlayerCell”,這將會解決Xcode所警告的問題。
試着運行一個,發現什么都沒變,這並不奇怪,因為我們還沒有給這個表格設置一個數據來源(DataSource),用以顯示。
新建一個文件,使用UIViewContoller模板,命名為 PlayersViewController ,設置喂UITableViewController的子類,不要勾選建立XIB文件。
回到Storyboard編輯器,選擇Table View Controller,在身份控制器中,把他的類設置為PlayerViewController,這對於把Storyboard中的場景和你自定義的子類掛鈎是十分重要的。要是不這么做,你的子類根本沒用。

現在起,當你運行這個應用時,table view controller其實是PlayersViewContoller的一個實例。
在 PlayersViewController.h 中聲明一個MutableArray(可變數組)
- #import <UIKit/UIKit.h>
- @interface PlayersViewController : UITableViewController
- @property (nonatomic, strong) NSMutableArray *players;
- @end
這個數組將會包含我們的應用的主要數據模型。我們現在加一些東西到這個數組之中,新建一個使用Obj-c模板的文件,命名為player,設置喂NSObject的子類,這將會作為數組的數據容器。
編寫Player.h如下:
- @interface Player : NSObject
- @property (nonatomic, copy) NSString *name;
- @property (nonatomic, copy) NSString *game;
- @property (nonatomic, assign) int rating;
- @end
編寫Player.m如下:
- #import "Player.h"
- @implementation Player
- @synthesize name;
- @synthesize game;
- @synthesize rating;
- @end
這里沒有什么復雜的,Player類只是一個容器罷了,包含三個內容:選手的名字、項目和他的評級。
接下來我們在App Delegate中聲明數組和一些Player對象,並把他們分配給PlayerViewController的players屬性。
在AppDelegate.m中,分別引入(import)Player和PlayerViewController這兩個類,之后新增一個名叫players的可變數組。
- #import "AppDelegate.h"
- #import "Player.h"
- #import "PlayersViewController.h"
- @implementation AppDelegate {
- NSMutableArray *players;
- }
- // Rest of file...
修改didFinishLaunchingWithOptions方法如下:
- - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
- {
- players = [NSMutableArray arrayWithCapacity:20];
- Player *player = [[Player alloc] init];
- player.name = @"Bill Evans";
- player.game = @"Tic-Tac-Toe";
- player.rating = 4;
- [players addObject:player];
- player = [[Player alloc] init];
- player.name = @"Oscar Peterson";
- player.game = @"Spin the Bottle";
- player.rating = 5;
- [players addObject:player];
- player = [[Player alloc] init];
- player.name = @"Dave Brubeck";
- player.game = @"Texas Hold’em Poker";
- player.rating = 2;
- [players addObject:player];
- UITabBarController *tabBarController =
- (UITabBarController *)self.window.rootViewController;
- UINavigationController *navigationController =
- [[tabBarController viewControllers] objectAtIndex:0];
- PlayersViewController *playersViewController =
- [[navigationController viewControllers] objectAtIndex:0];
- playersViewController.players = players;
- return YES;
- }
這將會創造一些Player對象並把他們加到數組中去。之后在加入:
- UITabBarController *tabBarController = (UITabBarController *)
- self.window.rootViewController;
- UINavigationController *navigationController =
- [[tabBarController viewControllers] objectAtIndex:0];
- PlayersViewController *playersViewController =
- [[navigationController viewControllers] objectAtIndex:0];
- playersViewController.players = players;
咦,這是什么?目前的情況是:我們希望能夠將players數組連接到PlayersViewController的players屬性之中以便讓這個VC能夠用做數據來源。但是app delegate根本不了解PlayerViewController究竟是什么,他將需要在storyboard中尋找它。
這是一個我不是很喜歡storyboard特性,在IB中,你在MainWindow.xib中總是會有一個指向App delegate的選項,在那里你可以在頂級的ViewController中向Appdelegate設置輸出口,但是在Storyboard中目前這還不可能,目前只能通過代碼來做這樣的事情。
- UITabBarController *tabBarController = (UITabBarController *)
- self.window.rootViewController;
我們知道storyboard的起始場景是Tab Bar Controller,所以我們可以直接到這個場景的第一個子場景來設置數據源。
PlayersViewController 在一個NavController的框架之中,所以我們先看一看UINavigationController類:
- UINavigationController *navigationController = [[tabBarController
- viewControllers] objectAtIndex:0];
然后詢問它的根試圖控制器,哪一個是我們要找的PlayersViewController:
- PlayersViewController *playersViewController =
- [[navigationController viewControllers] objectAtIndex:0];
但是,UIViewController根本就沒有一個rootViewController屬性,所以我們不能把數組加入進去,他又一個topViewController但是指向最上層的視圖,與我們這里的意圖沒有關系。
現在我們有了一個裝在了players物體合集的數組,我們繼續為PlayersViewController設置數據源。
打開PlayersViewController.m,加入以下數據源方法:
- - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- {
- return 1;
- }
- - (NSInteger)tableView:(UITableView *)tableView
- numberOfRowsInSection:(NSInteger)section
- {
- return [self.players count];
- }
真正起作用的代碼在cellForRowAtIndexPath方法里,默認的模板是如下這樣的:
- - (UITableViewCell *)tableView:(UITableView *)tableView
- cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- static NSString *CellIdentifier = @"Cell";
- UITableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:CellIdentifier];
- if (cell == nil) {
- cell = [[UITableViewCell alloc]
- initWithStyle:UITableViewCellStyleDefault
- reuseIdentifier:CellIdentifier];
- }
- // Configure the cell...
- return cell;
- }
無疑這就是以前設置一個表格視圖的方法,不過現在已經革新了,把這些代碼修改如下:
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- UITableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:@"PlayerCell"];
- Player *player = [self.players objectAtIndex:indexPath.row];
- cell.textLabel.text = player.name;
- cell.detailTextLabel.text = player.game;
- return cell;
- }
這看上去簡單多了,為了新建單元格,你只需使用如下代碼:
- UITableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:@"PlayerCell"];
如果沒有現存的單元格可以回收,程序會自動創造一個原型單元格的復制品之后返回給你,你只需要提供你之前在Storyboard編輯視圖中設置的身份證就可以的,在這里就是“PlayerCell”,如果不設置這個,這個程序就無法工作。
由於這個類對於Player容器目前一無所知,所以我們需要在文件的開頭加入一個引入來源
- #import "Player.h"
記得要創建synthesize語句哦親
- @synthesize players;
現在運行應用,會看到Table里有着players容器。

請注意:我們這里只使用一種單元格原型,如果你需要使用不同類型的單元格的話,只需要在storyboard中另外加入一個單元格原型就可以了,不過不要忘記給他們指派不同的身份證。
設計自定義的原型單元格
對於很多應用來說,使用默認的單元格風格就OK了,但是我偏偏要在每一個單元格的右邊加上一個一個圖片來表示選手的評級,但是添加圖片對於默認類型的單元格來說並不支持,我們需要自定義一個設計。
讓我們轉回MainStoryboard.storyboard,選中table view中的prototype cell,把它的Style attribute改為Custom,所有默認的標簽都會消失。
首先把單元格變得更高一些,你可以直接拉它,也可以在大小控制器中修改數字,我在這里使用55點的高度。
從 Objects Library中拖出兩個標簽物體,按照之前的樣式安插到單元格里,記得設置label的Highlighted顏色為白色,那樣的話當單元格被選中的時候會看起來更好看一些。
之后添加一個Image View對象,將它放置在單元格的右邊,設置他的寬度為81點,高度並不重要,在屬性檢查器中設置模式為置中。
我把標簽設置為210點長以確保他不會和ImageView重合,最后整體的設計會看起來象下面這樣:

由於這是一個自定義的單元格,所以我們不能夠使用UITableView默認的textLabel和detailLabel來設置數據,這些屬性也不再指向我們的單元格了,我們使用標簽(tags)來指定標簽。
將Name標簽的tag設置為100,Game的設置喂101,image的設置喂102,在屬性檢查器里設置哦親。
之后打開 PlayersViewController.m ,在PlayersViewcontroller中將cellForRowatIndexPath修改為:
- - (UITableViewCell *)tableView:(UITableView *)tableView
- cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- UITableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:@"PlayerCell"];
- Player *player = [self.players objectAtIndex:indexPath.row];
- UILabel *nameLabel = (UILabel *)[cell viewWithTag:100];
- nameLabel.text = player.name;
- UILabel *gameLabel = (UILabel *)[cell viewWithTag:101];
- gameLabel.text = player.name;
- UIImageView * ratingImageView = (UIImageView *)
- [cell viewWithTag:102];
- ratingImageView.image = [self imageForRating:player.rating];
- return cell;
- }
這里是用了一個新的方法,叫做ImageRating,在 cellForRowAtIndexPath方法之前加入這個方法:
- - (UIImage *)imageForRating:(int)rating
- {
- switch (rating)
- {
- case 1: return [UIImage imageNamed:@"1StarSmall.png"];
- case 2: return [UIImage imageNamed:@"2StarsSmall.png"];
- case 3: return [UIImage imageNamed:@"3StarsSmall.png"];
- case 4: return [UIImage imageNamed:@"4StarsSmall.png"];
- case 5: return [UIImage imageNamed:@"5StarsSmall.png"];
- }
- return nil;
- }
這就完成了,運行看看:

這和我們想象的結果並不是很符合,我們修改了原型單元格的屬性和高度,但是table view卻沒有考慮進去,有兩種方法可以修復它,我們可以改變table view的行高或者加入 heightForRowAtIndexPath 方法來修改,地一種方法更簡單,我們就用他。
注意:在一下兩種情況下,你應該使用 heightForRowAtIndexPath 方法:一是,你不能預先知道你的單元格的高度,二是不同的單元格會有不同的高度。
回到MainStoryboard.storyboard,在大小檢查器中將高度設置為55:

通過這種方式的話,如果之前你是使用拖動而不是鍵入數值的方式改變高度的屬性的話,則table view的數值也會自動改變。
現在運行看看,好多了吧
為原型單元格設置子類
我們的表格視圖已經相當像模像樣了,但是我並不是很喜歡使用tag來訪問label,要是我們能夠把這些lable連接到輸出口,之后在回應屬性中使用他們,該多好,而且不出所料,我們可以這樣做。
使用 Objective-C class模板新建一個文件,命名為PlayerCell,繼承UITableViewCell。
修改PlayerCell.h
- @interface PlayerCell : UITableViewCell
- @property (nonatomic, strong) IBOutlet UILabel *nameLabel;
- @property (nonatomic, strong) IBOutlet UILabel *gameLabel;
- @property (nonatomic, strong) IBOutlet UIImageView
- *ratingImageView;
- @end
修改PlayerCell.m
- #import "PlayerCell.h"
- @implementation PlayerCell
- @synthesize nameLabel;
- @synthesize gameLabel;
- @synthesize ratingImageView;
- @end
這個類本身並不其很大的作用,只是為nameLabel、gameLabel和ratingImageView聲明了屬性。
回到MainStoryboard.storyboard選中原型單元格,將他的class屬性修改為“PlayerCell”,現在當你向table view請求dequeueReusableCellWithIdentifier,他會返回一個PlayerCell實例而不是一個普通的UITableViewCell實例。
請注意我將這個類和reuse Indetifier的名字命名的一樣,只是營衛我喜歡這樣哦親,這兩個之間其實沒啥關系。
現在你可以將標簽和image view連接到輸出口去了,選中或者將他從鏈接檢查器拖動到table view cell。

請注意:要把這個control連接到table view cell而不是view controller哦親,別選錯了。
現在我們把一切都鏈接好了,只需要加入數據源的代碼就可以了。
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- PlayerCell *cell = (PlayerCell *)[tableView
- dequeueReusableCellWithIdentifier:@"PlayerCell"];
- Player *player = [self.players objectAtIndex:indexPath.row];
- cell.nameLabel.text = player.name;
- cell.gameLabel.text = player.game;
- cell.ratingImageView.image = [self
- imageForRating:player.rating];
- return cell;
- }
我們現在將接收到 dequeueReusableCellWithIdentifier 的控件指派到PlayerCell,只需要簡單的使用已經鏈接labels和image view到設置好的屬性上就可以了,這會讓這個設計看上去更加好控制,更加簡明。
當然,在PlayerCell前要引入資源:
- #import "PlayerCell.h"
試着運行,你會發現其實什么都沒有變化,可是我們都知道,內部已經有了變化。
在這相同的場景下面,我們可是在使用子類呢。
這里還有一些設計小竅門:第一點:一定要設置標簽被選中時的顏色。

第二點,確保你加入單元格的字符大小是可以變化的,這樣,當單元格大小變化時,他的內容的大小也會跟着變化,比如說:
在PlayersViewController.m中加入如下方法:
- - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
- {
- if (editingStyle == UITableViewCellEditingStyleDelete)
- {
- [self.players removeObjectAtIndex:indexPath.row];
- [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
- }
- }
這個方法加入好了之后,用手指輕掃一行單元格,會出現一個刪除鍵,試試看

Delete按鈕出現在右邊,遮住了一部分評級圖片,怎么解決呢?
打開MainStoryBoard.storyboard,選中table view cell中的image view,在大小檢查器中修改Autosizing屬性,是它能夠跟隨上級view的邊緣。

為labels設置同樣的屬性。

加入了這些變動之后,刪除按鈕如我們意料的出現了:

其實,最好的做法是讓這些星星在出現delete按鈕的時候消失,不過這只是一個練習,不要太較真哦親
如果你想了解更多Storyboard的特性,那么你就來對了地方,下面我們就來接着上次的內容詳細講解Storyboard的使用方法。
在上一篇《Storyboard全解析-第一部分》中,我們介紹了如何使用storyboard來制作多種場景和如何將這些場景鏈接起來,我們還學習了如何自定義一個表格視圖。
接下來這部分,也是最后一部分,我們將講解聯線(segue),靜態單元格等內容,我們還將加入一個選手詳細內容頁面,和一個游戲選擇頁面。
Segues的介紹
現在,讓我們創建一個場景使用戶可以自己增加新的選手進入列表。
在Players界面中拖入一個Bar Button,放置在導航欄的右側,在屬性監視器中將他的Identifier改為“add”,這樣他就會顯示一個加號的按鈕,當用戶點擊這個按鈕時,他就會彈出一個新的場景讓用戶對新的內容進行編輯或添加。
在編輯器中拖入一個新的Table View Controller,放置在Players場景的右邊,然后按住ctrl,拉動加號鍵到新的場景中,這樣,這個場景就會自動和這個按鈕建立聯系,從而自動歸入Navigation View Controller中。
放開鼠標之后,會出現如下選項:
選中Modal,你可以注意到出現了一種新的箭頭形式:
這種鏈接形式被官方稱為segue(pronounce: seg-way),我叫它聯線,(其實是轉換的意思)這種形式的聯線是表示從一種場景轉換到另外一種場景中,之前我們使用的連接都是描述一種場景包含另一種場景的。而對於聯線來說,它會改變屏幕中顯示的內容,而且必須由交互動作觸發:如輕點,或其他手勢。
聯線真正了不起的地方在於:你不再需要寫任何代碼來轉入一個新的場景,也不用在將你的按鈕和IBAction連接到一起,我們剛才做的,直接將按鈕和場景鏈接起來,就能夠完成這項工作。
運行這個app,按下 + 鍵,會發現出現了一個新的列表。
這種叫做 “modal” segue(模態轉換),新的場景完全蓋住了舊的那個。用戶無法再與上一個場景交互,除非他們先關閉這個場景,過一會我們會討論 push segue,這種segue會把場景推入導航棧。
新的場景現在還沒有什么用,你甚至不能把他關閉呢。
聯線只能夠把你送到新的場景,你要是想回來,就得使用delegate pattern,代理模式。我們必須首先給這個新的場景設置一個獨有的類,新建一個繼承UITableViewController的類,命為PlayerDetailsViewController。
為了把它和storyboard相連,回到MainStoryBoard,選擇新建的那個Table View Contrller,將他的類設置喂PlayerDetailViewController,千萬不要忘記這一步,這很重要。
做完這一步之后,把新場景的標題改為“Add Player”,分別加入“Done”和“Cancel”兩個導航欄按鈕。
修改PlayerDetailsViewController.h 如下:
這會聲明一個新的代理機制,當用戶點擊Cancel或者done按鈕時,我們將用它來交互Add Player場景和主場景通訊。
回到故事版編輯器,將Cancel和Done按鈕分別與動作方法連接,一種方式是,按住Ctrl拖動到ViewController上,之后選擇正確的動作。
在 PlayerDetailsViewController.m,加入如下代碼:
這是兩個導航欄按鈕要使用的方法,現在只需要讓代理知道我們剛才加入了代碼,而真正關閉場景只是代理的事情。
一般來說一定要為代理制定一個對象參數,這樣他才知道向那里發送信息。
不要忘記加入Synthesize語句。
現在我們已經為PlayerDetailsViewController設置了一個代理協議,我們需要將這個協議的實現方法(implement)寫在什么地方,很明顯應該寫在PlayerViewController因為這個vc代表了Add Player場景。在PlayersViewController.h中加入如下代碼:
並在PlayersViewController.m的結尾加入:
目前這個代理方法只能夠跳轉到這個新的場景中,接下來我們來讓他做一些更為強大的事情。
iOS 5 SDK中新添加的dismissViewControllerAnimated:completion: 方法可以被用來關閉一個場景。
最后還有一件事情需要做,就是Players場景需要告訴PlayerDetailsVC他的代理在哪里,聽上去這種工作在故事版編輯其中一拖就行了,實際上,你得使用代碼才能完成。
將以下方法加入到 PlayersViewController 中
當使用Segue的時候,就必須加入這個名叫 prepareForSegue 的方法,這個新的ViewController在被加載的時候還是不可見的,我們可以利用這個機會來向他發送數據。
請注意,這個segue的最終目標是Navigation Controller,因為這個是我們鏈接在導航欄上的按鈕,為了獲取PlayerDetailsViewController實例,我們必須通過NavController的屬性來獲取。
試着運行一下這個應用,單擊 + 鍵,然后試着關閉Add Player場景,仍然不管用。
這是因為我們沒有給Segue指定一個identifier,而parepareForSegu需要檢查AddPlayer的身份證,這是必須的,因為你有可能會同時使用多個聯線。
為了解決這個問題,進入Storyboard的編輯器,點擊Players場景和NavgationViewController場景之間的聯線,你會注意到與這個連線相關的按鈕會自動亮起來。
在屬性監視器中,將Identifier設置喂“AddPlayer”
如果這是你再次運行這個應用,點擊“Cancel”或者“Done”按鈕,這個場景就會自動關閉並且返回到上一級場景。
注意:從modal場景調用dismissViewControllerAnimated:completion方法是我們在這里使用的,但是這並不意味着你必須這樣做。但是,如果你不是代理來完成這個關閉窗口的工作的話,唯一需要注意的是,如果你之前使用了[self.parentViewController dismissModalViewControllerAnimated:YES] 語句來關閉窗口的話,那么這個語句就不會正常工作了。
順便說一下,屬性檢查器中有一個Transition的選項,在這里你可以選擇場景轉換是的動畫效果。
試着運行一下,看看那種動畫你最喜歡吧,但事情不要改變Style這個選項,如果你改變了,這個app可能會crash哦。
我們接下來在這個教程中還會用到幾次代理方法,下面我們來列一下為了完成一個連線,你需要做的幾件事情。
我們在這里必須使用代理,是因為根本沒有反向聯線這種東西,當sugue被啟動之后,他將會創造出一個目標場景的新實例。你當然可以做一個從目標場景回到原始場景的聯線,但是結果可能與你希望的大相徑庭。
距離來說吧,如果你做一條從cancel按鈕回到原始場景的連線的話,他並不會關閉當前場景並返回原始場景,而是會創建一個原始場景的新實例,這種情況會不停循環,知道把內存耗盡為止。
所以請記住:segue只用於打開新的場景。
靜態單元格
當我們全部完成之后,Add Player場景會看上去象下面的一樣:
這是一種分組表格視圖,但是不同的是,我們並不需要為這個表哥創建一個數據源,我們可以在故事版編輯器中直接設計這個視圖,而不需要重寫cellForRowAtIndex方法,使得我們可以這樣做的秘訣就是靜態單元格。
選中Add Player場景,之后在屬性檢查器中,將Content屬性改為StaticCell,將Style to Grouped屬性修改為2。
當你修改Section屬性時,編輯器會復制一個現有的組。你也可以自己選中一個組后選擇Duplicate。
我們的這個場景每個組只需要用一個行,所以選中上面的那個行之后刪除。
選中頂行,修改Header的值為:“Player Name”.
拖一個新的Text Field進入這個組的單元格里,把它的邊界刪除掉,使用System 17字體,取消Adjust to Fit選項。
我們現在在PlayerDetailsViewController中使用Assistant Editor這個Xcode 4.x的新特性來創建一個輸出口給這個Text Field,在工具欄的按鈕中打開Assistant Editor,那玩意看起來像個外星人,我指的是按鈕。
選中text field,按住Ctrl,將他拖到打開的文件之中。
放開鼠標,會出現一個選單。
將這個新的書出口命名為nameTextField,在你確定鏈接之后,Xcode會自動創建下列代碼:
他還會自動創建Synthesize語句,並同時在viewDidLoad文件中創建方法。
永遠別在動態表格中使用這種拖來拖去的方法,但是對於靜態單元格來說就OK,對於每個靜態單元格來說都必須創建一個新的實例。
將第二個組的靜態單元格的Style設置為Right Detail,這將會創建一個標准的單元格,把左側的label的內容修改為Game,設置一個Disclosure Indicator,為右側Detail的label設置一個輸出口。
最終的設計完成后是這樣的:
當你使用靜態單元格的時候,你的Table View Controller就不需要一個數據源了,但是因為我們使用了Xcode的模板來創造PlayerDetailsViewController這個類,他里面仍然有一些默認的數據源設置代碼,讓我們來刪除之。在以下這個標志
和這個標志之間的代碼全部刪除。
現在運行這個App,效果不錯吧,請注意我們不但一行代碼也沒寫,還刪除了好些。
但是我們並不能夠完全避免寫任何代碼,你可能已經注意到了,在文本框和單元格周圍有一些空間,用戶在完成編輯之后單擊這些區域並不會結束鍵盤什么的,怎么避免這個問題呢?用下面的代碼代替tableView:didSelectRowatIndex方法。
這些代碼就是說:如果用戶點擊第一個單元格后我們激活text field控件,這雖然是細節,但是細節決定成敗。
同時你也需要在屬性檢查器的Selection Style選項改為None。
OK,我們的設計全部完成了。
增加一個選手吧
現在我們暫時先忽略Game這一行,先讓用戶能夠編輯選手的情況之后再說。
當用戶單擊Cancel鍵的時候,不管作出什么修改都會被棄置,場景也會關閉並返回上一級菜單。這一塊的程序我們已經做好了,也就是我們剛才做得一個代理方法,它接收到did cancel這個方法之后就會關閉這個視圖。
但是當用戶單擊“Done”這個按鈕時,我們應該創建一個新的選手項目然后加入他的屬性,之后我們還需要通知代理器我們新增了一個選手,以便它能夠更新上一級菜單。
在 PlayerDetailsViewController.m,把完成的方法改成:
這需要我們引進Player的頭文件:
這個完成方法會創建一個新的Player實例,並把它發送給代理器,由於目前代理器還沒有這個方法,所以我們需要在PlayerDetailsViewController的頭文件中修改如下代碼:
這個“Did Save”的方法的聲明沒有了,我們加入一個“didAddPlayer”方法。
下面我們需要在執行文件中加入執行的方法,打開PlayersViewController.m,加入:
第一個語句向players的數組中加入新的Player對象,之后他會通知表格視圖:一個新的行已經被創建,這是因為table view和他的數據源必須一直是同步的才行,我們其實也可以使用[self.tableView reloadData]這個語句,但是重新創建一個單元格會有隨之而來的動畫,看起來更好看一些。UITableViewRowAnimationAutomatic是一個iOS 5的新特性,使各行自動選擇合適的動畫效果出現,非常好用。
現在試試看,你應該可以使用按鈕加入新行到表視圖中了。
如果你已經開始擔心storyboard的性能了,那么不用擔心。就算是將所有的場景都一塊載入的話,也不會消耗多少資源的。storyboard不會一下子加載所有的ViewController,而是會加載起始場景,在這里是Tab View,再從起始場景加載其他與起始場景相關的場景。
但是其他場景知道聯線到他們之前是不會被加載的。而這些場景在你返回之后都會卸載,所以只有當前場景會在內存中,就像你之前在用分開的nib文件一樣的。
我們通過實驗來看一看。在PlayerDetailsViewController.m中加入下面的方法:
我們重寫了initWithCoder和dealloc方法,使得debug控制台輸出一個很長的信息。這時候運行這個app,你會發現除非按下segue的按鈕,否則新的場景不會被初始化,放心了吧。
還有一件關於靜態單元格的事情需要注意,那就是他們只能夠在UITableViewController的子類下使用,如果他的父類不是UITableViewController,Xcode會提示下面的錯誤:
“Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances”.
原型單元格,雖然可以在普通的View Controller中使用,但是不能夠在Interface Builder中使用,
很少會出現有人會想要在一個表中用靜態單元格和原型單元格混合起來,目前iOS SDK還不能很好的支持這種方法。
游戲選擇器場景
在Add Player場景中單擊Game的單元格會打開一個新的場景,讓你能夠從一個列表中選擇一個游戲,這意味着我們需要加入一個新的表格視圖,不過不同的是,我們這次會使用push到Navigation的棧之中,而不是直接跳轉。
拖拉一個新的TableViewController到編輯器中,在Add Player場景中選擇一個單元格按住ctrl鍵拉到新的場景中,創建一個連線,選擇Push,之后把新segue的identifier命名為“PickGame”。
雙擊導航欄,修改標題為“Choose Game”,修改原型單元格的Style為Basic,修改他的Identifier為“GameCell”,我們的試圖設計就到這里。
新建一個UITableViewController的子類,命名為GamePickerViewController,在storyboard中也要設置好哦。
首先我們給這個新的場景一些數據來顯示,在GamePickerViewController.h中加入下列變量:
之后轉到GamePickerViewController.m,在viewDidLoad方法中加入數組的內容。
由於在viewDidLoad方法中加載了數組,所以需要在viewDidUnload中卸載之。
將模板中的數據源方法修改為如下代碼:
這樣我們就完成了家在數據源的方法,這時候運行這個app,之后在Add Player場景中單擊Game欄,就會轉入這個視圖了,但這時候單擊這里的單元格並不會有什么作用。
這時候,由於我們使用push方式將這個場景推進了Navigation的棧中,所以這時候我們單擊返回按鈕就會自動返回到上一級界面。不錯吧!
當然了,如果這個場景不輸送任何數據回到上一級場景的話,那他就什么用也沒有了,所以我們要創造一個新的代理器來完成這項任務。在GamePickerViewController.h中加入:
我們加入了一個代理方法,其中只有一個方法和一個用於乘放目前選擇的游戲的名字的屬性。
現在,我們修改GamePickerViewController.m的開頭:
這些代碼新建了一個數組,一個選中項目的整數,並且synthesize了這些項目。
在viewDidLoad中加入如下代碼:
選中的游戲名字會設置在self.game中,這里我們設置我們在表格中到底選中了哪個游戲。在這里,在場景加載之前必須首先填充self.game,由於我們在viewDidLoad之前設置了prepareForSegue這個方法,所以我們現在這么做沒問題。
修改cellForRowAtIndexPath方法:
這個方法會在選中的項目的右邊加上一個選中的對勾。
將 didSelectRowAtIndexPath 修改為:
首先我們取消之前點擊的那一行的選中狀態,這將把它的藍色變會正常的白色,之后將對勾刪除掉,之后將對勾放置在剛剛選中的那一行上,最后,我們把選中的那一行返回給代理。
現在運行這個app測試一下效果,單擊一個game的名字,將會出現一個對勾,單擊另一個行,對勾的位置就會改變,但是返回上一級菜單之后發現我們的修改沒有保存下來,為什么?因為我們還沒有將代理真正的鏈接起來。
在 PlayerDetailsViewController.h 中,引入
之后在 @interface 行之后加入:
在PlayerDetailsViewController.m加入prepareForSegue方法:
這和我們之前做過的很相似,但是這次的目標view Controller使game picker場景了,請記住,這個方法必須在GamePickerViewController初始化之后但是還沒有加載view的時候調用。
“game”變量是新的,我們必須聲明他:
我們使用這個變量來記錄到底選擇了哪個Game,我們得給這個String設置一個默認值,可以用initWithCoder方法來完成。
如果你之前是用過nibs的話,那么initWithCode可能會對你很熟悉,這部分在storyboard是一樣的。
修改 viewDidLoad 方法如下,以便單元格能夠顯示選中的Game名稱:
最后要做的就是執行代理方法:
這行代碼很好懂,我就不多講了。
我們的結束方法將會把選中的游戲的名字加入到新建的Player對象中。
OK,到這里我們就完成了游戲選擇器的場景,不錯吧。
在上一篇《Storyboard全解析-第一部分》中,我們介紹了如何使用storyboard來制作多種場景和如何將這些場景鏈接起來,我們還學習了如何自定義一個表格視圖。
接下來這部分,也是最后一部分,我們將講解聯線(segue),靜態單元格等內容,我們還將加入一個選手詳細內容頁面,和一個游戲選擇頁面。
Segues的介紹
現在,讓我們創建一個場景使用戶可以自己增加新的選手進入列表。
在Players界面中拖入一個Bar Button,放置在導航欄的右側,在屬性監視器中將他的Identifier改為“add”,這樣他就會顯示一個加號的按鈕,當用戶點擊這個按鈕時,他就會彈出一個新的場景讓用戶對新的內容進行編輯或添加。
在編輯器中拖入一個新的Table View Controller,放置在Players場景的右邊,然后按住ctrl,拉動加號鍵到新的場景中,這樣,這個場景就會自動和這個按鈕建立聯系,從而自動歸入Navigation View Controller中。

放開鼠標之后,會出現如下選項:

選中Modal,你可以注意到出現了一種新的箭頭形式:

這種鏈接形式被官方稱為segue(pronounce: seg-way),我叫它聯線,(其實是轉換的意思)這種形式的聯線是表示從一種場景轉換到另外一種場景中,之前我們使用的連接都是描述一種場景包含另一種場景的。而對於聯線來說,它會改變屏幕中顯示的內容,而且必須由交互動作觸發:如輕點,或其他手勢。
聯線真正了不起的地方在於:你不再需要寫任何代碼來轉入一個新的場景,也不用在將你的按鈕和IBAction連接到一起,我們剛才做的,直接將按鈕和場景鏈接起來,就能夠完成這項工作。
運行這個app,按下 + 鍵,會發現出現了一個新的列表。

這種叫做 “modal” segue(模態轉換),新的場景完全蓋住了舊的那個。用戶無法再與上一個場景交互,除非他們先關閉這個場景,過一會我們會討論 push segue,這種segue會把場景推入導航棧。
新的場景現在還沒有什么用,你甚至不能把他關閉呢。
聯線只能夠把你送到新的場景,你要是想回來,就得使用delegate pattern,代理模式。我們必須首先給這個新的場景設置一個獨有的類,新建一個繼承UITableViewController的類,命為PlayerDetailsViewController。
為了把它和storyboard相連,回到MainStoryBoard,選擇新建的那個Table View Contrller,將他的類設置喂PlayerDetailViewController,千萬不要忘記這一步,這很重要。
做完這一步之后,把新場景的標題改為“Add Player”,分別加入“Done”和“Cancel”兩個導航欄按鈕。

修改PlayerDetailsViewController.h 如下:
- @class PlayerDetailsViewController;
- @protocol PlayerDetailsViewControllerDelegate <NSObject>
- - (void)playerDetailsViewControllerDidCancel:
- (PlayerDetailsViewController *)controller;
- - (void)playerDetailsViewControllerDidSave:
- (PlayerDetailsViewController *)controller;
- @end
- @interface PlayerDetailsViewController : UITableViewController
- @property (nonatomic, weak) id <PlayerDetailsViewControllerDelegate> delegate;
- - (IBAction)cancel:(id)sender;
- - (IBAction)done:(id)sender;
- @end
這會聲明一個新的代理機制,當用戶點擊Cancel或者done按鈕時,我們將用它來交互Add Player場景和主場景通訊。
回到故事版編輯器,將Cancel和Done按鈕分別與動作方法連接,一種方式是,按住Ctrl拖動到ViewController上,之后選擇正確的動作。

在 PlayerDetailsViewController.m,加入如下代碼:
- - (IBAction)cancel:(id)sender
- {
- [self.delegate playerDetailsViewControllerDidCancel:self];
- }
- - (IBAction)done:(id)sender
- {
- [self.delegate playerDetailsViewControllerDidSave:self];
- }
這是兩個導航欄按鈕要使用的方法,現在只需要讓代理知道我們剛才加入了代碼,而真正關閉場景只是代理的事情。
一般來說一定要為代理制定一個對象參數,這樣他才知道向那里發送信息。
不要忘記加入Synthesize語句。
- @synthesize delegate;
現在我們已經為PlayerDetailsViewController設置了一個代理協議,我們需要將這個協議的實現方法(implement)寫在什么地方,很明顯應該寫在PlayerViewController因為這個vc代表了Add Player場景。在PlayersViewController.h中加入如下代碼:
- #import "PlayerDetailsViewController.h"
- @interface PlayersViewController : UITableViewController <PlayerDetailsViewControllerDelegate>
並在PlayersViewController.m的結尾加入:
- #pragma mark - PlayerDetailsViewControllerDelegate
- - (void)playerDetailsViewControllerDidCancel:
- (PlayerDetailsViewController *)controller
- {
- [self dismissViewControllerAnimated:YES completion:nil];
- }
- - (void)playerDetailsViewControllerDidSave:
- (PlayerDetailsViewController *)controller
- {
- [self dismissViewControllerAnimated:YES completion:nil];
- }
目前這個代理方法只能夠跳轉到這個新的場景中,接下來我們來讓他做一些更為強大的事情。
iOS 5 SDK中新添加的dismissViewControllerAnimated:completion: 方法可以被用來關閉一個場景。
最后還有一件事情需要做,就是Players場景需要告訴PlayerDetailsVC他的代理在哪里,聽上去這種工作在故事版編輯其中一拖就行了,實際上,你得使用代碼才能完成。
將以下方法加入到 PlayersViewController 中
- - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
- {
- if ([segue.identifier isEqualToString:@"AddPlayer"])
- {
- UINavigationController *navigationController =
- segue.destinationViewController;
- PlayerDetailsViewController
- *playerDetailsViewController =
- [[navigationController viewControllers]
- objectAtIndex:0];
- playerDetailsViewController.delegate = self;
- }
- }
當使用Segue的時候,就必須加入這個名叫 prepareForSegue 的方法,這個新的ViewController在被加載的時候還是不可見的,我們可以利用這個機會來向他發送數據。
請注意,這個segue的最終目標是Navigation Controller,因為這個是我們鏈接在導航欄上的按鈕,為了獲取PlayerDetailsViewController實例,我們必須通過NavController的屬性來獲取。
試着運行一下這個應用,單擊 + 鍵,然后試着關閉Add Player場景,仍然不管用。
這是因為我們沒有給Segue指定一個identifier,而parepareForSegu需要檢查AddPlayer的身份證,這是必須的,因為你有可能會同時使用多個聯線。
為了解決這個問題,進入Storyboard的編輯器,點擊Players場景和NavgationViewController場景之間的聯線,你會注意到與這個連線相關的按鈕會自動亮起來。
在屬性監視器中,將Identifier設置喂“AddPlayer”

如果這是你再次運行這個應用,點擊“Cancel”或者“Done”按鈕,這個場景就會自動關閉並且返回到上一級場景。
注意:從modal場景調用dismissViewControllerAnimated:completion方法是我們在這里使用的,但是這並不意味着你必須這樣做。但是,如果你不是代理來完成這個關閉窗口的工作的話,唯一需要注意的是,如果你之前使用了[self.parentViewController dismissModalViewControllerAnimated:YES] 語句來關閉窗口的話,那么這個語句就不會正常工作了。
順便說一下,屬性檢查器中有一個Transition的選項,在這里你可以選擇場景轉換是的動畫效果。

試着運行一下,看看那種動畫你最喜歡吧,但事情不要改變Style這個選項,如果你改變了,這個app可能會crash哦。
我們接下來在這個教程中還會用到幾次代理方法,下面我們來列一下為了完成一個連線,你需要做的幾件事情。
- 首先,從起始的控件做一條聯線到目標場景。
- 將這個聯線制定一個獨特的Identifier。
- 為目標場景制作一個代理方法。
- 在Cancel和Done按鈕,以及所有其他你需要和原始場景交流的地方調用代理方法。
- 在原始場景執行代理方法,這將會在用戶按下按鈕后關閉場景。
- 在原始場景執行prepareForSegue方法。
我們在這里必須使用代理,是因為根本沒有反向聯線這種東西,當sugue被啟動之后,他將會創造出一個目標場景的新實例。你當然可以做一個從目標場景回到原始場景的聯線,但是結果可能與你希望的大相徑庭。
距離來說吧,如果你做一條從cancel按鈕回到原始場景的連線的話,他並不會關閉當前場景並返回原始場景,而是會創建一個原始場景的新實例,這種情況會不停循環,知道把內存耗盡為止。
所以請記住:segue只用於打開新的場景。
靜態單元格
當我們全部完成之后,Add Player場景會看上去象下面的一樣:

這是一種分組表格視圖,但是不同的是,我們並不需要為這個表哥創建一個數據源,我們可以在故事版編輯器中直接設計這個視圖,而不需要重寫cellForRowAtIndex方法,使得我們可以這樣做的秘訣就是靜態單元格。
選中Add Player場景,之后在屬性檢查器中,將Content屬性改為StaticCell,將Style to Grouped屬性修改為2。

當你修改Section屬性時,編輯器會復制一個現有的組。你也可以自己選中一個組后選擇Duplicate。
我們的這個場景每個組只需要用一個行,所以選中上面的那個行之后刪除。
選中頂行,修改Header的值為:“Player Name”.

拖一個新的Text Field進入這個組的單元格里,把它的邊界刪除掉,使用System 17字體,取消Adjust to Fit選項。
我們現在在PlayerDetailsViewController中使用Assistant Editor這個Xcode 4.x的新特性來創建一個輸出口給這個Text Field,在工具欄的按鈕中打開Assistant Editor,那玩意看起來像個外星人,我指的是按鈕。
選中text field,按住Ctrl,將他拖到打開的文件之中。

放開鼠標,會出現一個選單。

將這個新的書出口命名為nameTextField,在你確定鏈接之后,Xcode會自動創建下列代碼:
- @property (strong, nonatomic) IBOutlet UITextField *nameTextField;
他還會自動創建Synthesize語句,並同時在viewDidLoad文件中創建方法。
永遠別在動態表格中使用這種拖來拖去的方法,但是對於靜態單元格來說就OK,對於每個靜態單元格來說都必須創建一個新的實例。
將第二個組的靜態單元格的Style設置為Right Detail,這將會創建一個標准的單元格,把左側的label的內容修改為Game,設置一個Disclosure Indicator,為右側Detail的label設置一個輸出口。
最終的設計完成后是這樣的:

當你使用靜態單元格的時候,你的Table View Controller就不需要一個數據源了,但是因為我們使用了Xcode的模板來創造PlayerDetailsViewController這個類,他里面仍然有一些默認的數據源設置代碼,讓我們來刪除之。在以下這個標志
- #pragma mark - Table view data source
和這個標志之間的代碼全部刪除。
- #pragma mark - Table view delegate
現在運行這個App,效果不錯吧,請注意我們不但一行代碼也沒寫,還刪除了好些。
但是我們並不能夠完全避免寫任何代碼,你可能已經注意到了,在文本框和單元格周圍有一些空間,用戶在完成編輯之后單擊這些區域並不會結束鍵盤什么的,怎么避免這個問題呢?用下面的代碼代替tableView:didSelectRowatIndex方法。
- - (void)tableView:(UITableView *)tableView
- didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- {
- if (indexPath.section == 0)
- [self.nameTextField becomeFirstResponder];
- }
這些代碼就是說:如果用戶點擊第一個單元格后我們激活text field控件,這雖然是細節,但是細節決定成敗。
同時你也需要在屬性檢查器的Selection Style選項改為None。
OK,我們的設計全部完成了。
增加一個選手吧
現在我們暫時先忽略Game這一行,先讓用戶能夠編輯選手的情況之后再說。
當用戶單擊Cancel鍵的時候,不管作出什么修改都會被棄置,場景也會關閉並返回上一級菜單。這一塊的程序我們已經做好了,也就是我們剛才做得一個代理方法,它接收到did cancel這個方法之后就會關閉這個視圖。
但是當用戶單擊“Done”這個按鈕時,我們應該創建一個新的選手項目然后加入他的屬性,之后我們還需要通知代理器我們新增了一個選手,以便它能夠更新上一級菜單。
在 PlayerDetailsViewController.m,把完成的方法改成:
- - (IBAction)done:(id)sender
- {
- Player *player = [[Player alloc] init];
- player.name = self.nameTextField.text;
- player.game = @"Chess";
- player.rating = 1;
- [self.delegate playerDetailsViewController:self
- didAddPlayer:player];
- }
這需要我們引進Player的頭文件:
- #import "Player.h"
這個完成方法會創建一個新的Player實例,並把它發送給代理器,由於目前代理器還沒有這個方法,所以我們需要在PlayerDetailsViewController的頭文件中修改如下代碼:
- @class Player;
- @protocol PlayerDetailsViewControllerDelegate <NSObject>
- - (void)playerDetailsViewControllerDidCancel:
- (PlayerDetailsViewController *)controller;
- - (void)playerDetailsViewController:
- (PlayerDetailsViewController *)controller
- didAddPlayer:(Player *)player;
- @end
這個“Did Save”的方法的聲明沒有了,我們加入一個“didAddPlayer”方法。
下面我們需要在執行文件中加入執行的方法,打開PlayersViewController.m,加入:
- - (void)playerDetailsViewController:
- (PlayerDetailsViewController *)controller
- didAddPlayer:(Player *)player
- {
- [self.players addObject:player];
- NSIndexPath *indexPath =
- [NSIndexPath indexPathForRow:[self.players count] - 1
- inSection:0];
- [self.tableView insertRowsAtIndexPaths:
- [NSArray arrayWithObject:indexPath]
- withRowAnimation:UITableViewRowAnimationAutomatic];
- [self dismissViewControllerAnimated:YES completion:nil];
- }
第一個語句向players的數組中加入新的Player對象,之后他會通知表格視圖:一個新的行已經被創建,這是因為table view和他的數據源必須一直是同步的才行,我們其實也可以使用[self.tableView reloadData]這個語句,但是重新創建一個單元格會有隨之而來的動畫,看起來更好看一些。UITableViewRowAnimationAutomatic是一個iOS 5的新特性,使各行自動選擇合適的動畫效果出現,非常好用。
現在試試看,你應該可以使用按鈕加入新行到表視圖中了。
如果你已經開始擔心storyboard的性能了,那么不用擔心。就算是將所有的場景都一塊載入的話,也不會消耗多少資源的。storyboard不會一下子加載所有的ViewController,而是會加載起始場景,在這里是Tab View,再從起始場景加載其他與起始場景相關的場景。
但是其他場景知道聯線到他們之前是不會被加載的。而這些場景在你返回之后都會卸載,所以只有當前場景會在內存中,就像你之前在用分開的nib文件一樣的。
我們通過實驗來看一看。在PlayerDetailsViewController.m中加入下面的方法:
- - (id)initWithCoder:(NSCoder *)aDecoder
- {
- if ((self = [super initWithCoder:aDecoder]))
- {
- NSLog(@"init PlayerDetailsViewController");
- }
- return self;
- }
- - (void)dealloc
- {
- NSLog(@"dealloc PlayerDetailsViewController");
- }
我們重寫了initWithCoder和dealloc方法,使得debug控制台輸出一個很長的信息。這時候運行這個app,你會發現除非按下segue的按鈕,否則新的場景不會被初始化,放心了吧。
還有一件關於靜態單元格的事情需要注意,那就是他們只能夠在UITableViewController的子類下使用,如果他的父類不是UITableViewController,Xcode會提示下面的錯誤:
“Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances”.
原型單元格,雖然可以在普通的View Controller中使用,但是不能夠在Interface Builder中使用,
很少會出現有人會想要在一個表中用靜態單元格和原型單元格混合起來,目前iOS SDK還不能很好的支持這種方法。
游戲選擇器場景
在Add Player場景中單擊Game的單元格會打開一個新的場景,讓你能夠從一個列表中選擇一個游戲,這意味着我們需要加入一個新的表格視圖,不過不同的是,我們這次會使用push到Navigation的棧之中,而不是直接跳轉。
拖拉一個新的TableViewController到編輯器中,在Add Player場景中選擇一個單元格按住ctrl鍵拉到新的場景中,創建一個連線,選擇Push,之后把新segue的identifier命名為“PickGame”。
雙擊導航欄,修改標題為“Choose Game”,修改原型單元格的Style為Basic,修改他的Identifier為“GameCell”,我們的試圖設計就到這里。

新建一個UITableViewController的子類,命名為GamePickerViewController,在storyboard中也要設置好哦。
首先我們給這個新的場景一些數據來顯示,在GamePickerViewController.h中加入下列變量:
- @interface GamePickerViewController : UITableViewController {
- NSArray * games;
- }
之后轉到GamePickerViewController.m,在viewDidLoad方法中加入數組的內容。
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- games = [NSArray arrayWithObjects:
- @"Angry Birds",
- @"Chess",
- @"Russian Roulette",
- @"Spin the Bottle",
- @"Texas Hold’em Poker",
- @"Tic-Tac-Toe",
- nil];
- }
由於在viewDidLoad方法中加載了數組,所以需要在viewDidUnload中卸載之。
- - (void)viewDidUnload
- {
- [super viewDidUnload];
- games = nil;
- }
將模板中的數據源方法修改為如下代碼:
- - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- {
- return 1;
- }
- - (NSInteger)tableView:(UITableView *)tableView
- numberOfRowsInSection:(NSInteger)section
- {
- return [games count];
- }
- - (UITableViewCell *)tableView:(UITableView *)tableView
- cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- UITableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:@"GameCell"];
- cell.textLabel.text = [games objectAtIndex:indexPath.row];
- return cell;
- }
這樣我們就完成了家在數據源的方法,這時候運行這個app,之后在Add Player場景中單擊Game欄,就會轉入這個視圖了,但這時候單擊這里的單元格並不會有什么作用。

這時候,由於我們使用push方式將這個場景推進了Navigation的棧中,所以這時候我們單擊返回按鈕就會自動返回到上一級界面。不錯吧!
當然了,如果這個場景不輸送任何數據回到上一級場景的話,那他就什么用也沒有了,所以我們要創造一個新的代理器來完成這項任務。在GamePickerViewController.h中加入:
- @class GamePickerViewController;
- @protocol GamePickerViewControllerDelegate <NSObject>
- - (void)gamePickerViewController:
- (GamePickerViewController *)controller
- didSelectGame:(NSString *)game;
- @end
- @interface GamePickerViewController : UITableViewController
- @property (nonatomic, weak) id <GamePickerViewControllerDelegate> delegate;
- @property (nonatomic, strong) NSString *game;
- @end
我們加入了一個代理方法,其中只有一個方法和一個用於乘放目前選擇的游戲的名字的屬性。
現在,我們修改GamePickerViewController.m的開頭:
- @implementation GamePickerViewController
- {
- NSArray *games;
- NSUInteger selectedIndex;
- }
- @synthesize delegate;
- @synthesize game;
這些代碼新建了一個數組,一個選中項目的整數,並且synthesize了這些項目。
在viewDidLoad中加入如下代碼:
- selectedIndex = [games indexOfObject:self.game];
選中的游戲名字會設置在self.game中,這里我們設置我們在表格中到底選中了哪個游戲。在這里,在場景加載之前必須首先填充self.game,由於我們在viewDidLoad之前設置了prepareForSegue這個方法,所以我們現在這么做沒問題。
修改cellForRowAtIndexPath方法:
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- UITableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:@"GameCell"];
- cell.textLabel.text = [games objectAtIndex:indexPath.row];
- if (indexPath.row == selectedIndex)
- cell.accessoryType =
- UITableViewCellAccessoryCheckmark;
- else
- cell.accessoryType = UITableViewCellAccessoryNone;
- return cell;
- }
這個方法會在選中的項目的右邊加上一個選中的對勾。
將 didSelectRowAtIndexPath 修改為:
- - (void)tableView:(UITableView *)tableView
- didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- {
- [tableView deselectRowAtIndexPath:indexPath animated:YES];
- if (selectedIndex != NSNotFound)
- {
- UITableViewCell *cell = [tableView
- cellForRowAtIndexPath:[NSIndexPath
- indexPathForRow:selectedIndex inSection:0]];
- cell.accessoryType = UITableViewCellAccessoryNone;
- }
- selectedIndex = indexPath.row;
- UITableViewCell *cell =
- [tableView cellForRowAtIndexPath:indexPath];
- cell.accessoryType = UITableViewCellAccessoryCheckmark;
- NSString *theGame = [games objectAtIndex:indexPath.row];
- [self.delegate gamePickerViewController:self
- didSelectGame:theGame];
- }
首先我們取消之前點擊的那一行的選中狀態,這將把它的藍色變會正常的白色,之后將對勾刪除掉,之后將對勾放置在剛剛選中的那一行上,最后,我們把選中的那一行返回給代理。
現在運行這個app測試一下效果,單擊一個game的名字,將會出現一個對勾,單擊另一個行,對勾的位置就會改變,但是返回上一級菜單之后發現我們的修改沒有保存下來,為什么?因為我們還沒有將代理真正的鏈接起來。
在 PlayerDetailsViewController.h 中,引入
- #import "GamePickerViewController.h"
之后在 @interface 行之后加入:
- @interface PlayerDetailsViewController : UITableViewController <GamePickerViewControllerDelegate>
在PlayerDetailsViewController.m加入prepareForSegue方法:
- - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
- {
- if ([segue.identifier isEqualToString:@"PickGame"])
- {
- GamePickerViewController *gamePickerViewController =
- segue.destinationViewController;
- gamePickerViewController.delegate = self;
- gamePickerViewController.game = game;
- }
- }
這和我們之前做過的很相似,但是這次的目標view Controller使game picker場景了,請記住,這個方法必須在GamePickerViewController初始化之后但是還沒有加載view的時候調用。
“game”變量是新的,我們必須聲明他:
- @implementation PlayerDetailsViewController
- {
- NSString *game;
- }
我們使用這個變量來記錄到底選擇了哪個Game,我們得給這個String設置一個默認值,可以用initWithCoder方法來完成。
- - (id)initWithCoder:(NSCoder *)aDecoder
- {
- if ((self = [super initWithCoder:aDecoder]))
- {
- NSLog(@"init PlayerDetailsViewController");
- game = @"Chess";
- }
- return self;
- }
如果你之前是用過nibs的話,那么initWithCode可能會對你很熟悉,這部分在storyboard是一樣的。
修改 viewDidLoad 方法如下,以便單元格能夠顯示選中的Game名稱:
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- self.detailLabel.text = game;
- }
最后要做的就是執行代理方法:
- #pragma mark - GamePickerViewControllerDelegate
- - (void)gamePickerViewController:
- (GamePickerViewController *)controller
- didSelectGame:(NSString *)theGame
- {
- game = theGame;
- self.detailLabel.text = game;
- [self.navigationController popViewControllerAnimated:YES];
- }
這行代碼很好懂,我就不多講了。
我們的結束方法將會把選中的游戲的名字加入到新建的Player對象中。
- - (IBAction)done:(id)sender
- {
- Player *player = [[Player alloc] init];
- player.name = self.nameTextField.text;
- player.game = game;
- player.rating = 1;
- [self.delegate playerDetailsViewController:self didAddPlayer:player];
- }
OK,到這里我們就完成了游戲選擇器的場景,不錯吧。
