Storyboard入門

Storyboard 是iOS 5 中令人興奮的一個新特性,他將爲你在創建用戶界面上節省很多時間。 那麼究竟什麼是Storyboard呢?我將用一幅圖片來向你展示: 下面這個就是本教程中即將用到的Storyboard。

The full storyboard we'll be making in this tutorial.

你或許不能精確的知道這個應用是做什麼的,但是你可以清楚的看到它有哪些屏幕界面,這些屏幕界面之間是怎樣互相關聯的。這就是Storyboard的強大之處。

如果你的應用有很多個不同的屏幕界面,Storyboard則可以減少那些用於在這些界面之前來回切換的中間代碼(glue code)。 現在你的應用,用一個Storyboard就可以包含所有控制器的界面設計和他們之間的關係, 而不再需要爲每一個控制器分別再創建一個nib文件。

Storyboard和普通的nib相比有很多優點。

  • 藉助Storyboard,你可以對你應用中所有的界面和它們之間的聯繫有一個更好的概念上的總覽。 因爲所有的設計都在單個文件中,而不是分佈成許多nib文件, 可以更加容易的找到任何東西。
  • Storyboard表明了各個界面之間的切換規則。 這些切換規則叫做“segues”, 按住ctrl鍵,從一個控制器拖動到另一個就可以創建它們。多虧了segues,能讓你用更少的代碼來處理UI。
  • Storyboard讓UITableView使用起來更加簡單, 它提供了原型單元格(prototype cells)和靜態單元格(static cells). 你幾乎可以完全在Storyboard編輯器中來設計你的UITableView,大大減少了你的代碼量。

並不是所有的事情都那麼完美,當然,Storyboard也有一些侷限性。 Storyboard編輯器還沒有像Interface Builder那麼強大,還有少數的一些功能,IB可以實現,但是Storyboard編輯器不能完成。你也需要一個大顯示器,特別是在設計iPad應用 時。

如果你是那種討厭使用Interface Builder,只願意編碼實現整個UI的人, 那麼Storyboard大概不是爲你準備的。 從我個人來說,我更希望代碼量越少越好,特別是UI代碼,所以這個工具對我來說可是個好東西。

你還可以在iOS 5 和Xcode 4.2中使用nib文件。 雖然我們現在有了Storyboard,使用Interface Builder也不是不可以。 如果你要繼續用nib並且一直用下去, 但是你也可以將Storyboard和nib一起使用。 這不是一個必須二選一個問題。

在本教程中,我們將會看到你可以用Storyboard來做什麼。 我們將要創建的應用可能會看起來沒什麼意義,但是它向你展示Storyboard最常用的那些操作。

 

開始吧

打開Xcode並且創建一個新項目。 使用Single View Application模板作爲我們的起點。然後從這裏開始構建我們的應用。

Xcode template options

如下填寫模板選項:

  • Product Name: Ratings
  • Company Identifier: the identifier that you use for your apps, in reverse domain notation
  • Class Prefix: leave this empty
  • Device Family: iPhone
  • Use Storyboard: check this
  • Use Automatic Reference Counting: check this
  • Include Unit Tests: this should be unchecked

Xcode創建好項目後,Xcode主窗口顯示如下:

Xcode window after creating project

我們的新項目裏有兩個類,AppDelegate和ViewController, 還有這個教程的主角 MainStoryboard.storyboard。 注意了,這次項目中沒有 .xib 文件,也沒有MainWindow.xib。

讓我們看一看Storyboard。 在左邊的項目導航中點擊MainStoryboard.storyboard 來打開Storyboard編輯器:

Storyboard editor

Storyboard編輯器看起來很像Interface Builder。 你可以從Object Library(屏幕右下角)中拖動控件到View Controller中來設計佈局。 不同的是,Storyboard不是僅僅包含一個控制器,而是你應用中所有的控制器。

控制器,在Storyboard的官方術語中叫做”scene”, 不過 “scene“ 其實就是一個視圖控制器。 在這之前,你會爲每一個Scene/控制器來創建一個nib文件, 但是現在所有的這些都包含在一個Storyboard中。

在iPhone中一次只能顯示這些scene中的一個, 但是在iPad中,你可以同時顯示多個, 例如UISplitView或者是Popover。

爲了進一步的感受編輯器是如何工作的, 拖動一下控件到控制器的空白區域吧。

Dragging controls from Object Library

左邊欄是文檔的大綱:

Document outline

在Interface Builder中,這個區域列出了nib中的組件,但是在Storyboard編輯器中,他顯示了所有的視圖控制器中的內容。現在我們的Storyboard中僅僅有一個視圖控制器,但是隨着教程的演進,我們將會添加更多的控制器。

在 Scene 下方,有一個迷你版的文檔大綱,叫做Dock:

The dock in the Storyboard Editor

Dock顯示了scene中最頂級的組件。 每個scene至少有一個First Responder和一個View Controller對象, 但是他也可以擁有其他的頂級組件。 後面會更多的講解。 Dock可以方便的建立連接。 如果你想將一些其他的東西連接到控制器, 你可以把他的圖標拖動到Dock上面。

注意: 你或許沒怎麼用過 First Responder 。 這是一個代理對象,用於指向當前作爲第一事件響應者(first responder)的對象。 它也出現在 Interface Builder 中, 你看可能從來沒有用過它。 舉個例子,你可以將一個按鈕的 Touch Up Inside 事件綁定到 First Responder 的 cut: selector 上。 如果在某一時刻,一個文本框得到了焦點, 那麼你就可以按下那個按鈕,讓這個作爲First Responder 的文本框,來剪切它裏面的文本到剪貼板上。

運行這個應用, 你應該可以看到它顯示的內容和我們在編輯器中設計的完全一樣:

App with objects

如果你之前創建過基於nib的應用,那麼你會一直有一個MainWindow.xib文件。 這個nib文件包含了一個頂級元素UIWindow, 一個App Delegate的引用,和一個或多個視圖控制器。 當你將應用的UI搬到Storyboard後, 就不再需要MainWindow.xib文件了。

No more MainWindow.xib

那麼,如果沒有MainWindow.xib文件, Storyboard是如何被加載的呢?

讓我們來看一下應用的delegate文件, 打開AppDelegate.h, 你將會看到如下內容:

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

要使用Storyboard,你的應用代理對象就必須繼承UIResponder(以前都是直接繼承自NSObject),並且還有一個UIWindow屬性(和以前相比,這個屬性不再是一個IBOutlet)。

如果你再看一下 AppDelegate.m , 你會發現它沒有做任何事情, 所有的方法都是空的。 即使是 application:didFinishLaunchingWithOptions: 方法, 也只不過簡單的返回了一個YES。 在以前,這裏會把主視圖控制器的視圖添加到window上面,或者將window設置到rootViewController屬性上面,但是現在,這些都 不需要了。

這個祕密就在於Info.plist文件中, 點擊Ratings-Info.plist文件(在Supporting Files分組中), 你將會看到下面的內容:

Setting the main storyboard file base name in Info.plist

在基於nib的項目中,這裏會有一個名爲NSMainNibFile鍵,或者叫”Main nib file base name”, 它會告訴UIApplication去加載MainWindow.xib, 然後把它關聯到應用中。 而我們現在的Info.plist已經沒有這些設置了。

Storyboard應用會使用一個叫做UIMainStoryboardFile的鍵,或者叫做“Main storyboard file base name”, 來指定應用啓動時要加載的Storyboard名稱。 當檢測到這個設置後,UIApplication將會加載 MainStoryboard.storyboard 文件,並且自動實例化其中的第一個視圖控制器, 同時把它的所有視圖放到一個新的UIWindow對象中。 不需要寫任何代碼。

你也可以在Target Summary中看到這些:

Setting Main Storyboard in Target summary

這裏面有一個新的iPhone/iPod Deployment Info選項讓你來選擇是使用Storyboard還是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() 函數的最後一個參數是一個nil值,現在他是NSStringFromClass([AppDelegate class])。

和使用MainWindow.xib最大的不同就是,應用代理不是Storyboard的一部分。因爲應用代理不再從nib文件中加載,我們就必須告訴UIApplicationMain我們的應用代理的名字,否則就找不到它了。

把它添加到我的標籤上

我們的評分應用使用一個有兩個標籤的標籤化界面。通過Storyboard可以非常容易的創建標籤。

切換回到MainStoryboard.storyboard,從Object Library中拖動一個Tab Bar Controller到設計器中。 你或許想把Xcode的窗口最大化,因爲Tab Bar Controller還包含了兩個視圖控制器,所以你需要一些空間來擺放他們。

Adding a new tab bar controller into the Storyboard

這個新的Tab Bar Controller包含了兩個預先配置好的子控制器,每個都對應了它的一個Tab。UITabBarController是一個容器控制器,因爲他包含 了一個或多個的子控制器。 還有其他兩個容器分別是Navigation Controller和Split View Controller(我們將在後面看到它們)。 iOS 5 另外一個很酷的新增特性就是提供了一個新的API讓你可以寫自己的容器控制器。 在本書的後續章節,我們會有一個教程來說明它。

在Storyboard編輯器中,通過箭頭連接Tab Bar Controller和它包含的視圖控制器來表示容器中的關係。

Relationship arrow in the Storyboard editor

注意:如果你想同時移動Tab Bar controller和它的子控制器,你可以按住Command鍵並點擊選擇多個scene,然後再移動它們(選中的scene會有一個淺藍色的邊框)。

拖動一個Label到第一個視圖控制器中,設置他的文本爲”First Tab”。 同樣也拖動一個Label到第二個控制器中,並且設置他的文本爲”Second Tab”。 這樣我們就可以清楚的看到在切換Tab時發生了什麼。

注意: 當編輯器被縮放時,你不能拖動任何東西到scene中, 所以,你首先需要返回到默認縮放級別中。

選中Tab Bar Controller並且找到Attributes Inspector。 選中內容爲Initial View Controller複選框。

Is Initial View Controller attribute

在編輯面板中,原來指向普通控制器的那個箭頭,現在指向了Tab Bar Controller:

Arrow indicating initial view controller in Storyboard editor

這樣,當我們啓動應用的時候,UIApplication就會將Tab Bar Controller作爲我們應用的第一個界面。

Storyboard總是會有一個試圖控制器用作初始控制器, 作爲Stroyboard的入口。

啓動這個應用試一試, 這個應用現在有一個Tab Bar並且你可以在這兩個Tab之間來回切換:

App with tab bar

Xcode實際上有一個用來創建標籤化應用的模板來供我們使用(不出意料,叫做Tabbed Application template),但是最好還是要知道它的原理, 這樣在必要的時候,你也可以手動創建它。

你可以刪除那個被模板默認添加進來的控制器,我們已經不再需要它了。 現在Storyboard僅包含了Tab Bar和它的兩個子控制器。

順便說一下,如果你連接多於5個scene到Tab Bar Controller中,它會自動放置一個 “更多” 標籤,非常漂亮。

增加一個Table View Controller

現在附加到Tab Bar Controller的兩個Scene都是普通的UIViewController。 我想把第一個標籤的scene替換成一個UITableViewController。

點擊第一個控制器, 選中它並且刪除它。 從Object Library中拖出一個新的Table View Controller到面板中。

Adding a new table view controller to the Storyboard

當Table View Controller被選中後,在Xcode的菜單欄中選擇 EditorEmbed InNavigation Controller。 這樣會增加另外一個控制器到面板中:

Embedding in a navigation controller

你也可以從Object Library中拖出一個Navigation Controller, 但是Embed In這個方式更加簡單。

因爲Navigation Controller也是一個容器控制器(和Tab Bar Controller一樣), 它有一個箭頭指向Table View Controller。你也可以在文檔大綱中看到他們之間的關係。

View controller relationships in outline of Storyboard editor

注意到,嵌入的Table View Controller包含有一個navigation bar。 Storyboard自動添加了這個,因爲這個Scene將會被顯示到Navigation Controller的框架中。 它不是一個真正的UINavigationBar, 而是用來模擬的。

如果你看一下Table View Controller的Attributes Inspector,可以在最上面看到模擬選項:

Simulated metrics in Storyboard editor

“Inferred” 是Storybard的默認設置, 它的意思是當一個Scene包含在navigation controller中的時候,將會顯示一個navigation bar, 同樣的,在tab bar controller中,會顯示一個tab bar。 你也可以覆蓋這個設置, 但要記住,他們只是用來幫你設計界面的。 這些模擬選項不會在運行時實際的出現,他們只是在設計界面時用來模擬運行時的最終效果。

讓我們把這些新的Scene連接到Tab Bar Controller中。 按住Ctrl然後從Tab Bar Controller拖動到Navigation Controller:

Connecting scenes in the storyboard

當你這樣做之後,一個小的彈出框顯示出來:

Create relationship segue

選擇 “Relationship – viewControllers” 這個選項。這樣會在這兩個Scene中創建一個新的關係:

Relationship arrow in the Storyboard editor

Tab Bar Controller有兩個這樣的關係,每個Tab對應一個。 Navigation Controller自己也有一個關係連接到Table View Controller。 還有另外一種箭頭, 叫做Segue,我們在後面就會討論它。

當我們建立了這個新的連接, 一個新的Tab就會被添加到Tab Bar Controller, 叫做 “Item”. 我想讓這個新的Scene作爲第一個Tab,所以,拖動這些Tab來改變它們的順序:

Rearranging tabs in the Storyboard editor

運行應用看一下, 第一個Tab現在包含了一個裏面有table view的navigation controller。

App with table view

在我們給應用增加一些實際功能之前,讓我們稍微整理一下Storyboard。 我想要把第一個Tab的名字改成”Players”,把第二個改成”Gestures”。 你不能通過Tab Bar Controller來改變他們, 但是你可以通過連接到每個Tab的視圖控制器來改變他們。

當你將一個視圖控制器連接到Tab Bar Controller後, 它會得到一個Tab Bar Item對象,你可以用Tab Bar Item來設置tab的標題和圖片。

在Navigation Controller中,選擇Tab Bar Item, 在Attributes Inspector中設置它的Title爲 “Players”:

Setting the title of a Tab Bar Item

把第二個Tab Bar Item的名稱改爲 “Gestures”。

我們應該也需要在這些tab上面放置一些圖片。在教程資源中包 含了一個Images子目錄。 把這個目錄添加到項目中。 在名爲”Players”的 Tab Bar Item 的 Attributes Inspector 中,選擇Players.png 這張圖片。 你可能已經猜到了, 也要爲”Gestures” 這個Tab Item設置Gestures.png這張圖片。

同樣的,Navigation Controller中的子控制器,也有一個Navigation Item用來配置導航條。 選中Table View Controller中的Navigation Item, 將它的Title改爲”Players”。

另外,你還可以雙擊導航條來改變它的Title。(注意:你應該雙擊Table View Controller中模擬的導航條,而不是Navigation Controller中的那個。)

Changing the title in a Navigation Item

運行應用,令人驚歎的事情發生了, 所有這些都沒有寫一行代碼!

App with the final tabs

原型單元格

你可能注意到了,當我們添加Table View Controller之後,Xcode出現了這樣的警告:

Xcode warning: Prototype cells must have reuse identifiers

警告的消息如是這樣,“Unsupported Configuration: Prototype table cells must have reuse identifiers”。當你添加Table View Controller到storyboard後, 它默認會使用原型單元格,但是我們並沒有正確的配置它,所以纔會出現警告。

原型單元格是Storyboard和相比nib之下,提供的一項非常酷的優勢特性。 以前, 如果你需要一個自定義的單元格,你只能用代碼爲單元格添加子視圖,或者爲單元格單獨創建一個nib文件,然後再通過一些特殊的技巧加載它。 這樣還是可行的,但是原型單元格讓這一切變得更加簡單。 現在,你可以直接在Storyboard編輯器中,設計你自定義的單元格。

Table View Controller默認有一個空的原型單元格。 選中它,然後在Attributes Inspector中設置Style爲Subtitle。這樣會立即將單元格的外觀改變成包含兩個Label單元格。 如果你之前使用過TableView並且創建過自己的單元格,你會發現這個和UITableViewCellStyleSubtitle是一樣的。 藉助原型單元格,你可以像我們剛纔那樣,使用任何一個內建的單元格樣式,或者創建一個你自己的樣式(這個是我們一會兒要做的)。

Creating a Prototype cell

設置Accessory屬性爲Disclosure Indicator, 並且把單元格的Reuse Identifier設置成“PlayerCell”。 這樣就可以消除Xcode的警告了。 所有的原型單元格仍然還是一個UITableViewCell對象,所以他們還是應該有一個Reuse Identifier。 Xcode只是確保我們不會忘了它(至少我們中會注意警告的人)。

運行應用, …沒有任何改變。 這並不奇怪,我沒有爲Table指定數據源,所以它還不知道該怎麼顯示每一行。

在項目中創建一個新文件。選擇 UIViewController 模板。將這個類命名爲PlayersViewController並且讓它繼承UITableViewController。 “With XIB for user interface option” 這個選項應該是未選中狀態, 因爲我們已經在storyboard中設計好了這個控制器。 今天沒有nibs!

Creating a view controller with the table view controller template

返回Storyboard編輯器, 選中Table View Controller, 在Identity Inspector 中,設置Class爲PlayersViewController。 這是將Storyboard中的Scene和你自己的控制器子類相關聯的關鍵一步。 千萬不要忘了這個,否則你自己的類不會被使用。

Setting the class name in the identity inspector

從現在開始,當你運行應用,Storyboard中的table view controller將會是PlayersViewController類的一個實例。

爲PlayersViewController.h增加一個可變數組的屬性:

#import <UIKit/UIKit.h>

@interface PlayersViewController : UITableViewController

@property (nonatomic, strong) NSMutableArray *players;

@end

這個數組將會包含我們應用主要的數據模型。 它包含Player對象。 讓我們現在就創建Player類。 用Objective-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是一個簡單的容器對象,有三個屬性: 玩家的名稱,他正在玩的遊戲名稱,還有評分(1星到5星)。

我們將在App Delegate中創建一個數組和一些用來測試的Player對象,然後把他們賦值給 PlayersViewController的playes屬性。

在 AppDelegate.m 中,導入Player和PlayersViewController類,並且增加一個新的實例變量 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對象並把它們添加到players數組中。 而我們是這樣做的:

 
UITabBarController *tabBarController = (UITabBarController *)
  self.window.rootViewController;
UINavigationController *navigationController = 
  [[tabBarController viewControllers] objectAtIndex:0];
PlayersViewController *playersViewController = 
  [[navigationController viewControllers] objectAtIndex:0];
playersViewController.players = players;

那是什麼呢? 我們想要把 players 數組賦值給 PlayersViewController 的 players 屬性, 然後它就可以用這個數組來作爲數據源。 但是應用代理還不知道 PlayersViewController 這個控制器, 所以我們需要通挖掘 Storyboard 來找到它。

這就是 Storyboard 的其中一點侷限性。 Interface Builder 總能夠在 MainWindow.xib 中讓應用代理中引用到, 並且你可以將頂級控制器連接到應用代理的 outlets 中。 目前這個在 Storyboard 中還是不行的。 你不能在應用代理中引用到頂級控制器。 這很不幸, 但是我們還是總能夠通過代碼來得到這些引用。

UITabBarController *tabBarController = (UITabBarController *)
  self.window.rootViewController;

我們知道,Storybord 的根控制器是一個 Tab Bar Controller, 所以我們可以找到 window 的 rootViewController 並強制轉換它。

PlayersViewController 在第一個Tab中的導航控制器內部, 所以我們可以先找到 UINavigationController 對象:

UINavigationController *navigationController = [[tabBarController 
  viewControllers] objectAtIndex:0];

然後將它當做我們要找的 PlayersViewController 的根控制器。

PlayersViewController *playersViewController = 
  [[navigationController viewControllers] objectAtIndex:0];

很不幸, UINavigationController 沒有 rootViewController 這個屬性, 所以我們需要使用 viewControllers 數組。(它倒有一個 topViewController 屬性,但是它指向的是棧中的最頂端控制器, 但我們找的是最下端的。 當應用剛啓動的時候,我們可以用 topViewController 來得到它, 但這種情況並不總是成立)。

現在,我們有一個充滿 Player 對象的數組, 我們可以繼續爲 PlayersViewController 構建數據源。

打開 PlayersViewController.m, 並修改Table View 的數據源方法:

 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return 1;
}

- (NSInteger)tableView:(UITableView *)tableView 
  numberOfRowsInSection:(NSInteger)section
{
	return [self.players count];
}

真正的功效發生在 cellForRowAtIndexPath 方法。 Xcode 模板生成的代碼是這樣的:

 
- (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;
}

這毫無疑問,是你一直以來創建你自己的 Table View 的代碼。 好吧,以後不會再用到了! 將這個方法替換成:

- (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編輯器中爲這個原型單元格指定的重用標識(Reuse Ifentifier), 在我們這個項目中就是 “PlayerCell”。 一定不要忘記設置這個標識,否則這個就不能正常工作了。

因爲這個類還不知道 Player 對象, 所以需要在文件頂部,增加一行 #import 語句。

#import "Player.h"

還有,我們別忘了給這個屬性加上 synthesize 語句:

@synthesize players;

現在,你可以運行應用了, 看!, Table View 中有了這些玩家信息了:

Table view with data

在這個應用中,我們只用了一個原型單元格, 但是如果你的 Table 需要顯示不同種類的單元格, 那麼你可以直接添加新的原型單元格到 Storyboard 中。 你可以複製現有的單元格,或者增加Table View 的 Prototype Cells 屬性的值。 不過要確保你給每一個原型單元格都指定了重用標識。

僅僅需要一行代碼,就可以使用原型單元格了。 我想,這太棒了!

設計你自己的原型單元格

對大多數應用來說,使用標準的單元格樣式已經足夠了, 但是, 我想在單元格的右邊添加一張圖片用來表示玩家的評分(用星星表示)。 標準的單元格樣式沒有提供這樣的外觀, 所以我們需要創建一個自定義個設計。

切換回到 MainStoryboard.storyboard, 選中Table View 的原型單元格, 設置 Style 屬性爲 Custom。 默認的Label 都消失了。

首先,我們讓單元格的高度大一些, 拖動單元格下邊框,或者在 Size Inspector 中改變 Row Height 屬性都可以。 我用第二種方法設置單元格的高度爲 55 點。

從 Objects Library 中拖出兩個 Label 到單元格中, 並且讓把兩個 Label 放在和之前差不多的位置。 調整一些字體和顏色屬性。 把他們的 Highlighted color 屬性都設置成 white。 這樣在用戶點擊單元格後單元格的背景變成藍色時,看起來會好看一點。

拖動一個 Image View 到單元格中,並放置到右邊, 緊挨着右邊的指示箭頭。 設置寬度爲81 點, 高度不是很重要。
爲了讓任何放到這裏的圖片都不會被拉伸, 設置它的 Mode 屬性爲 Center (在 Attributes Inspector 中的 View 選項卡中)。

我將Label都設置成210點寬,所以他們不會和 Image View 重疊上。 原型單元格最終的設計看起來是這樣的:

Prototype cells with a custom design

因爲這是一個自定義單元格, 我們不能再用 UITableViewCell 的 textLabel 和 detailTextLabel 屬性來放置文本。 這些屬性指向的 Label 已經不在我們的單元格上面了。 我們需要使用 tags 來找到我們的 Label。

將 Name Label 的 tag 設爲 100, Game Label 的tag 設爲 101, Image View 的 tag 設爲 102, 你可以在 Attributes Inspector 中設置它們。

然後打開 PlayersViewController.m , 修改 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;
}

這裏用到了一個新方法 imageForRating, 把這個方法增加到 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;
}

已經差不多了. 現在重新運行應用吧。

Wrong cell height for custom cells made in Storyboard editor

恩,看起來沒什麼問題。 我們修改了原型單元格的高度, 但是 Table View 沒有自動調整這些。 有兩個方法可以修正它: 我們可以修改 Table View 的 Row Height 屬性, 或者實現 heightForRowAtIndexPath 方法。 第一個方法更加簡單, 讓我們這樣弄吧。

注意: 如果你提前不知道單元格的高度, 或者每個單元格有不同的高度, 那麼你要用 heightForRowAtIndexPath 方法。

回到 MainStoryboard.storyboard, 在 Table View 的 Size Inspector 中, 設置行高爲 55:

Setting the table view row height

順便說一下, 如果你是通過拖動邊緣修改的單元格高度, 而不是輸入高度值。 那麼Table View 的 Row Height 也會自動跟着改變。

如果運行應用, 它看起來會好很多了。

使用原型單元格的子類

我們的 Table View 已經運轉的很不錯了, 但是我不太喜歡使用tag來訪問原型單元格的Label 和其他視圖。 如果我們能把Lable 連接到 outlet 上, 然後使用相應的屬性, 那就非常方便了。 結果證明, 我們可以:

在項目中增加一個新文件, 使用 Objective-C 模板。 文件名爲 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, 選中原型單元格, 在 Identity Inspector 中修改 Class 屬性爲 “PlayerCell” 。現在當你用 dequeueReusableCellWithIdentifier 來獲得新的單元格時, 它將返回而一個 PlayerCell 的實例, 而不再返回 UITableViewCell。

注意,我給這個類指定的名稱和重用標識是一樣的 — 他們都叫做 PlayerCell — 但這只是我爲了保持前後一致。 類名和重用標識並沒有什麼關係, 所以你也可以給他們指定不同的名字。

現在,你可以將 Label 和 Image View 都連接到這些 outlet 中了。 選擇label 從 Connections Inspector 中拖動到 Table View Cell 上面, 或者用另一種方式, 按住 Ctrl從 Table View 中拖回 Label。

Connecting the player cell

重要提示: 你用該將這些控件綁定到Table View Cell中, 而不是 View Controller! 你知道, 當你的數據源向Table View 請求一個新的單元格時, Table View 給你的不是原型單元格本身, 而是一個*拷貝*(或者,如果可能,一個之前被回收的單元格)。 這說明,會在任何一個時間,同時有多於一個 PlayerCell 實例存在。 如果你將單元格中的一個Label 連接到控制器的 Outlet上面, 那麼多個label的拷貝會嘗試使用同樣一個outlet。 這很快就會引起麻煩。 (另外,將原型單元格的事件連接到控制器上是沒問題的。 如果你的單元格上有自定義按鈕或其他控件, 你應該這樣做。)

現在,我們已經綁定好了屬性, 我們可以簡化我們的數據源代碼:

- (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, 然後我們可以直接使用這裏的 Label 和 Image View. 我確實很喜歡原型單元格的使用方式, 這讓Table View 少了很多垃圾代碼。

你還需要導入 PlayerCell 類:

#import "PlayerCell.h"

運行應用。 當你啓動應用後, 它看起來和之前一樣, 但在這一切的後面, 我們正在使用我們自定義的單元格。

這裏有一些設計上的技巧。 當你在設計自己的單元格時, 這裏有一些事情需要注意一下。 首先, 你應該設置 Label 的高亮顏色, 這樣當用戶點擊單元格式,他們看起來會比較好。

Selecting the proper highlight color

第二, 你要保證你加入的內容是靈活的, 當單元格的尺寸改變時,裏面內容的尺寸會隨着它調整。 當你爲單元格提供刪除和移動能力的時候, 單元格的尺寸就將會改變。

Segues 介紹

現在是時候爲我們的 Storyboard 添加更多的控制器了。 我們將要創建一個新界面,用來讓用戶增加新的玩家到應用中。

在Players界面上,拖動一個 Bar Button Item 到導航欄裏面的右邊。 在 Attributes Inspector 中修改它的 Identifier 爲 Add, 讓它變成一個標準的 + 按鈕。 當你點擊這個按鈕時, 我們將彈出一個模態界面讓你來輸入新用戶的詳細信息。

拖出一個新的 Table View Controller 到主面板上, 放在 Players 界面的右邊。 記住你可以在面板上雙擊鼠標來縮放面板, 這樣可以給你大的工作區域。

讓這個新的 Table View Controller 處於選中狀態,並且將他嵌入到一個 Navigation Controller 中(如果你忘了怎麼操作,在菜單欄中選擇 EditorEmbed InNavigation Controller).).

這裏有一個小技巧, 選中我們剛剛加入到Players界面中的 + 按鈕, 然後按住Ctrl拖動到新的 Navigation Controller:

Creating a segue in the storyboard editor

鬆開鼠標按鍵, 一個小彈出框就出現了:

Popup to choose Segue type - push, modal, or custom

選擇 Modal。 這樣就在 Players 界面和這個 Navigation Controller 之間創建了一個新類型的箭頭:

A new segue in the storyboard editor

這種類型的連接被稱之爲 segue(發音爲:seg-way)),它表示了從一個界面切換到另一個界面。 目前爲止我們建立的連接都是表示控制器之間的關係的。 而Segue, 和我們之前建立的連接不同, 它改變了屏幕上顯示的界面。 他們可以通過點擊按鈕,單元格,手勢等方式來觸發。

segue最酷的一件事就是,你不再需要爲展現新界面寫任何代碼了,也不需要爲你的按鈕綁定IBAction了。 我們剛纔做的,從 Bar Button Item 拖動到下一個界面, 這就足以創建這個界面切換了。 (如果你的控件已經綁定了 IBAction 事件, 那麼 segue 會覆蓋它)。

運行應用, 然後點擊這個 + 按鈕,一個新的Table View 將會從屏幕上滑出來。

App with modal segue

這就是 “Modal” segus. 新的界面完全遮蓋住了前一個界面。 直到關閉這個 Modal 界面,否則用戶是不能操作前一個界面的。 在後面, 我們還會看到 “Push” segue, 它會在導航欄的棧中壓入一個新的界面。

這個新界面還不是很有用 — 你甚至不能關閉它回到主界面!

Segue 是單向的, 從 Players 界面到這個新界面。 如果要返回, 我們必須用代理模式。 爲了這樣, 我們首先必須爲這個新界面定義它自己的類。 增加一個新的 UITableViewController 子類到項目中, 文件名叫做 PlayerDetailsViewController。

我們要綁定它到storyboard。 切換回 MainStoryboard.storyboard, 選中剛纔那個新建的 Table View Controller, 並且在 Identity Inspector 中設置 Class 爲 PlayerDetailsViewController。 我總是忘記這個重要的一步。 所以, 爲了確保你不會這樣, 我將會一直提到它。

把這個界面的 title 修改成 “Add Player” (雙擊導航條)。 增加兩個 Bar Button Item 到導航條上面, 在 Attributes Inspector 中, 分別設置兩個設置按鈕的 Identifier 屬性爲 Cancel 和 Done。

Setting the title of the navigation bar to

然後修改 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

這裏定義了一個新的代理協議, 當用戶點擊取消和完成按鈕時, 我們用它來從 Add Player 界面向主界面通信。

切換回 Storyboard 編輯器, 並將取消和完成按鈕分別綁定到它們的事件方法中去。 一個方法是, 按住 Ctrl 然後從按鈕拖動到控制器, 然後從彈出框中找到相應的動作名稱。

Connecting the action of a bar button item to the view controller in the storyboard editor

在 PlayerDetailsViewController.m 文件底部中,添加如下兩個方法:

- (IBAction)cancel:(id)sender
{
	[self.delegate playerDetailsViewControllerDidCancel:self];
}
- (IBAction)done:(id)sender
{
	[self.delegate playerDetailsViewControllerDidSave:self];
}

設置兩個導航欄按鈕的動作方法。 現在,它們只是讓代理對象知道發生了什麼事情。 具體要怎麼相應這些事件,就是代理對象的事了。 (這不是必須的,不過是我喜歡的一種方式。 還有,你也可以讓 Add Player 界面在通知它的代理之前,先關閉自己)。

注意一下,代理方法通常會用第一個參數來引用調用它的哪個對象, 在我們這裏是 PlayerDetailsViewController。 這個方式能讓代理知道是哪個對象給它發送的消息。

別忘了用 synthesize 定義一下 delegate 屬性。

@synthesize delegate;

現在我們給 PlayerDetailsViewController 添加了一個代理協議,我們還需要在一些地方實現他們。 很明顯, 應該在 PlayersViewController 中實現, 因爲是它展現的 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];
}

現在,代理方法只是簡單的關閉當前界面。 稍後我們會讓它做一些有趣的事情。

dismissViewControllerAnimated:completion: 這個方法是 iOS 5 新增的。 以前你可能用過 dismissModalViewControllerAnimated: 方法。 這個方法現在還可以繼續使用, 不過這個新版本的方法應該是首選(因爲他能讓你在界面消失後,執行一些附加的代碼)。

還有最後一件事要做: Players 界面必須告訴 PlayerDetailsViewController 它是它的代理。 簡單看起來, 這件事好像在 Storyboard 編輯器中拖動一條線就能完成。很不幸, 這樣不行。 使用segue的過程中,我們需要寫一些代碼才能傳送數據到新的控制器。

在 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 方法。 在這個方法調用時, 新的控制器已經被加載出來了,但是還沒有顯示出來, 我們可以利用這個機會來把數據傳遞給它。 (你不能自己調用 prepareForSegue, 這是 UIKit 用來通知你 segue 被觸發時調用的方法)。

注意這個 segue 的目標是 Navigation Controller, 因爲它是我們從 Bar Button Item 連接過去的。 要得到 PlayerDetailsViewController 的實例, 我們必須深度遍歷 Navigation Controller 的 viewControllers 屬性。

運行應用,按下 + 按鈕, 然後嘗試關閉 Add Player 界面, 它還是不能正常工作!

這是因爲我們沒有給Segue設置過標識(identifier)。 prepareForSegue 中的代碼檢測的是標識爲 AddPlayer 的 segue。 建議每次都進行這樣的檢測, 因爲你可能在一個控制器中有多個segue, 並且你需要區分他們(後面我們會做到這個)。

爲了解決這個問題, 我們進入Storyboard編輯器,並且點擊在 Players 界面和 Navigation Controller 之間的 segue。 注意到 Bar Button Item 被高亮顯示了, 所以你可以清楚的看到是哪個控件觸發的這個segue。

在 Attributes Inspector 中,設置 Identifier 屬性爲 “AddPlayer”:

Setting the segue identifier

再次運行這個應用, 點擊 Cancel 或者 Done 按鈕, 就會按照預期關閉當前界面並且返回到玩家列表界面。

注意: 完全可以在彈出的模態界面中調用 dismissViewControllerAnimated:completion: 方法。 並不是必須要在代理中使用這個方法。 不過,我個人比較喜歡把它放在代理中, 但是,如果你向讓模態界面來關閉它自己也是沒問題的。 有一點你需要注意的: 如果你使用以前的這個方法 [self.parentViewController dismissModalViewControllerAnimated:YES] 來關閉界面的話,它不會正常工作了。 現在可以用 self.presentingViewController 而不是 self.parentViewController 來調用這個方法, 這是iOS 5 新增加的一個屬性。

順便說一下,segue 的 Attributes Inspector 中, 有一個 Transition 屬性, 你可以選擇不同的切換動畫:

Setting the transition style for a segue

可以都試試他們,找到哪個是你最喜歡的。 但是不要改變 Style 設置。 因爲這個界面是模態的,修改其他選項會導致應用崩潰。

我們將會在教程中多次用到代理模式。 這裏是在兩個 scene 之間建立連接的步驟。

  1. 在源 scene 中的按鈕或其他控件上創建 segue 到目標 scene。(如果你想用模態的方式展現新界面, 那麼一般目標應該是 Navigation Controller)
  2. 爲 segue 設置一個唯一標識。(它僅僅需要在源 scene 中唯一, 不同的 scene 可以用同樣的標識名)
  3. 爲目標 scene 創建代理協議
  4. 調用 Cancel 和 Done 按鈕上的代理方法, 另一方面, 你的目標 Scene 需要和源 Scene 進行通訊。
  5. 讓源 Scene 實現代理協議。 它應該在 Cancel 或 Done 按鈕按下的時候關閉目標控制器。
  6. 在源 Scene 中實現 prepareForSegue 方法, 在這裏設置 destination.delegate = self; 。

因爲沒有反向的 Segue, 所以代理是必須要有的。 當 Segue 被觸發時,會創建一個新的目標控制器。 你當然可以從目標控制器用 Segue 返回源控制器, 但是這樣不會得到你期望的效果。

例如,如果你在 Cancel 按鈕上,創建了一個返回 Players 界面的 Segue, 它不會關閉新建玩家的界面,也不會返回到 Players 界面, 它會創建一個新的 Players 界面。 這樣你就陷入了一個死循環中, 直到應用的內存用盡。

記住:Segue 只能是單向的, 他們只能用於打開一個新界面。 如果要返回或者關閉界面(或者從導航控件的棧中彈出),通常都是用代理來做的。 Segue 僅僅聽從於源控制器, 而目標控制器甚至都不知道自己是被 Segue 打開的。

靜態單元格

當我們完成這些後, 增加玩家的界面應該是這樣:

The finished add Player screen

當然,這是一個分組的Table View,但不同的是,這次我們不用爲這個Table View 創建數據源了。 我們可以直接在 Storyboard 編輯器中設計它, 不需要 cellForRowAtIndexPath 方法了。 靜態單元格提供的特性,讓這成爲了可能。

在 Add Player 界面中選擇Table View, 並且在 Attributes Inspector 中修改 Content 的值爲 Static Cells。 設置 Style 屬性爲 Grouped, Sections 屬性爲 2。

Configuring a table view to use static cells in the storyboard editor

當你修改了 Sections 屬性, 編輯器會複製現有的 section。 (你也可以在左邊的文檔大綱中,選擇一個特定的 Section, 然後複製它)。

我們的界面中,每個 Section 只需要一行數據, 刪除掉那些多餘的單元格吧。

選中最上面的 section, 在 Attributes Inspector 中設置 Header 屬性爲 “Player Name”。

Setting the section header for a table view

拖動一個新的 Text Field 到這個 Section 的單元格中。 刪除它的邊框,這樣你就不能看到文本是從何處開始和結束的了。 設置字體爲 System 17, 並且取消 Adjust to Fit。

我們將在 PlayerDetailsViewController 中用 Xcode 的 Assistant Editor 功能爲這個文本框創建一個outlet。 用工具欄上的按鈕打開 Assistant Editor*(那個看起來想一個燕尾服的按鈕)。 它應該會自動打開 PlayerDetailsViewController.h 。

選中文本框, 然後按住 Ctrl 拖動到 .h 文件中:

Ctrl-dragging to connect to an outlet in the assistant editor

鬆開鼠標按鍵, 會出現一個彈出框:

The popup that shows when you connect a UITextField to an outlet

給這個新的 outlet 命名爲 nameTextField。 點擊 Connect 按鈕後, Xcode 會爲 PlayerDetailsViewController.h 增加如下的屬性:

@property (strong, nonatomic) IBOutlet UITextField *nameTextField;

它還會自動 synthesize 這個屬性,並把它添加到 .m 文件的 viewDidUnload 方法中。

我告訴過你,這種方式在原型單元格中是不能用的, 但是在靜態單元格中是可以的。 因爲每個靜態單元格只有一個實例(不像原型單元格, 他們從來不會被複制), 所以,將它們的子視圖連接到控制器上也是完全可以的。

設置第二個 Section 中的靜態單元格的 Style 屬性爲 Right Detail。 這給了我們一個標準的樣式去操作。 修改左邊的 Label 的文字爲 “Game”, 並且爲這個單元格設置一個向右指示的箭頭。 爲右邊的 Label(文本爲 “Detail” 的那個)創建一個 outlet, 並且命名爲 detailLabel。 這個單元格上面的 Label 只是普通的 UILabel 對象。

Add Player 界面的最終設計如下:

The final design of the Add Player screen

當你使用靜態單元格時, 你的 Table View 控制器不需要數據源。 因爲我們是用 Xcode 模板創建的 PlayerDetailsViewController 類, 它仍然會有數據源相關的默認代碼, 所以讓我們把這些代碼刪除掉吧, 現在我們不需要它了。 刪除下面兩個代碼斷之間的所有代碼:

#pragma mark - Table view data source

#pragma mark - Table view delegate

這樣應該會去掉 Xcode 關於這個類的所有警告消息。

運行應用, 看到新的界面是靜態單元格構成的。 所有這些都沒有寫過一行代碼 — 事實上, 我們還刪除了很多代碼。

我們應該把代碼寫完整, 當你把文本框添加到第一個單元格時, 你可能注意到它並沒有完全適應界面, 在文本框的周圍,有一個小小的外邊距。 用戶不能看到文本框是何處起始和結束的, 所以如果用戶點擊到外邊距區域,鍵盤就不會彈出來,這樣會給用戶造成迷惑。 爲了避免這種情況,我們應該在點擊單元格的任何區域時,都讓鍵盤彈出來。 這非常簡單, 僅僅需要覆蓋 tableView:didSelectRowAtIndexPath 方法:

- (void)tableView:(UITableView *)tableView 
  didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	if (indexPath.section == 0)
		[self.nameTextField becomeFirstResponder];
}

這個的意思是, 如果用戶點擊了第一個單元格, 我們就激活文本框(因爲每個單元格只有一行, 所以我們只需要判斷 section 的索引)。 這樣會自動的彈出鍵盤。 這只是一個小技巧, 但是這避免了用戶的迷惑。

你還應該在 Attributes Inspector, 中設置這個單元格的 Selection Style 屬性爲 None(而不是 Blue), 否則當用戶點擊文本框的外邊距部分時,單元格的背景會變成藍色。

好了, 這就是 Add Player 的設計部分了, 現在讓它來實際運轉起來吧。

讓 Add Player 界面工作起來

到現在,我們忽略了 Game 這行, 僅僅讓用戶在這裏輸入玩家的名稱,沒做任何其他的事情。

當用戶按下 Cancel 按鈕, 這個界面應該關閉, 這裏輸入的任何數據都將會丟失。 這個功能已經可以了。 代理對象(Players 界面)接受到 “did cancel” 消息, 然後直接關閉控制器。

當用戶按下 Done, 我們應該創建一個新的 Player 對象,然後設置好它的屬性。 然後, 我們應該告訴代理對象, 我們增加了一個新的玩家, 這樣, 它就可以更新它自己的界面了。

PlayerDetailsViewController.m 中,修改 done 方法:

- (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"

done 方法,現在創建了一個新的 Player 實例並且把它發送給代理。 代理協議現在還沒有這個方法, 所以,我們修改一些 PlayerDetailsViewController.h 的定義:

@class Player;

@protocol PlayerDetailsViewControllerDelegate <NSObject>
- (void)playerDetailsViewControllerDidCancel:
  (PlayerDetailsViewController *)controller;
- (void)playerDetailsViewController:
  (PlayerDetailsViewController *)controller 
  didAddPlayer:(Player *)player;
@end

“didSave” 方法的定義去掉了, 而我們現在有了 “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];
}

首先,把這個新的 Player 對象添加到 players 數組中。 然後告訴 Table View 增加了一行新數據(在最下面), 因爲 Table View 和它的數據源必須時刻保持同步。 我們可以直接用 [self.tableView reloadData] 方法, 但是用一個動畫插入新的一行,看起來會更好。 UITableViewRowAnimationAutomatic 是 iOS 5 中新提供的一個常量, 它會根據你要插入的行的位置,自動找到一個合適的動畫,非常方便。

試一下吧, 你現在應該可以把新的玩家增加到主界面的列表中了!

如果你對 storyboard 的性能好奇, 你應該知道一點,一次性加載整個 storyboard 不是一個大問題。 Storyboard 不會一次性的實例化它裏面的所有控制器, 只會實例化初始控制器。 因爲我們的初始控制器是一個 Tab Bar Controller, 它裏面包含的兩個子控制器也會被加載(Players Scene 和 第二個 Tab 中的 Scene)。

直到你用 segue 打開他們, 否則其他的控制器是不會被加載的。 當你關閉控制器後,他們又會被釋放掉, 所以僅有當前正在使用的控制器纔會在內存中, 和你用單獨的 nib 是一樣的。

讓我們實踐一下, 在 PlayerDetailsViewController.m 中增加如下方法:

- (id)initWithCoder:(NSCoder *)aDecoder
{
	if ((self = [super initWithCoder:aDecoder]))
	{
		NSLog(@"init PlayerDetailsViewController");
	}
	return self;
}
- (void)dealloc
{
	NSLog(@"dealloc PlayerDetailsViewController");
}

我們重寫了 initWithCoder 和 dealloc 方法,在裏面增加了一些日誌輸出。 現在,再次運行應用,並且打開 Add Player 界面。你應該會看到,這個控制器在這個時候,纔會被初始化。 當你關閉 Add Player 界面後,按下 Cancel 或者 Done按鈕, 你應該會看到 dealloc 中的 NSLog 輸出。如果你再次打開這個界面,你應該還會再看見 initWithCoder 中輸出的消息。 這樣會保證你的控制器只有在需要時纔會被加載, 就和手動的加載 nib 文件是一樣的。

關於靜態單元格另外一點,他們只能使用在 UITableViewController 中。 Storyboard 編輯器可以讓你把它們添加到普通的 UIViewController 中的 Table View 上面, 但是這個在運行時不會正常工作。 原因是, UITableViewController 內部有一些特殊的機制來管理靜態單元格的數據源。 Xcode 甚至通過顯示錯誤消息 “Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances” 來防止你這樣做。

另一方面, 原型單元格可以在 UIViewController 中很好的工作。 不過,這兩種單元格,都不能用在 Interface Builder 中。 目前,如果你想使用原型單元格或者是靜態單元格, 那麼你就必須使用 Storyboard。

你可能會想在同一個Table View 中同時使用靜態單元格和動態單元格, 但 SDK 還沒有很好的支持這項功能。 如果你確實想這樣做, 那麼看看這裏的一個解決方案。

注意: 如果你要創建一個有很多靜態單元格的界面 — 超出了可顯示區域 — 那麼你可以在 Storyboard 編輯器中使用鼠標或觸摸板(雙指滑動)上面的滾動手勢。 這個可能不是很明顯, 但是它確實可以用。

Game Picker 界面

點擊 Add Player 界面中的 Game 那行應該打開一個新的界面, 來讓用戶從一個列表中選擇一個遊戲。 這就是說,我們需要添加一個新的 Table View Controller, 然而這次我們將把它壓入導航欄的棧中,而不是模態的顯示它。

拖動一個新的 Table View Controller 到 storyboard 上面。 在 Add Player 界面中選擇 Game 單元格(要確保選中的是整個單元格, 而不是裏面的 Label), 然後按住 Ctrl 拖動到新的 Table View Controller 上, 在他們之間創建一個 segue。 創建一個 Push segue 並且給它的標識命名爲 “PickGame”。

雙擊導航條,設置它的名稱爲 “Choose Game”。 設置原型單元格的 Style 屬性爲 Basic, 並且設置 Reuse Identifier 爲 “GameCell”。 這就是所有我們需要對這個界面進行的設計:

The design for the Game Picker screen

增加一個新的 UITableViewController 子類,並且名稱爲 GamePickerViewController。 不要忘記把 storyboard 中的 Table View Controller 和這個類關聯起來。

首先,我們要給這個新的界面一些數據來顯示,爲 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;
}

即便實際上 viewDidUnload 不會被這個界面調用(我們沒有用另外一個視圖蓋住它), 不過這是一個好的實踐, 總是要平衡內存的分配和釋放。

替換用模板生成的數據源方法:

- (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;
}

只要數據源連接上,就會做這些事情。 運行應用, 然後點擊 Game 那行單元格。 這個新的選擇遊戲的界面就會滑出來。 現在點擊單元格不會有任何反應,但是這個界面展現在了導航欄棧上面,你可以按下返回按鈕來回到 Add Player 界面。

App with Choose Game screen

這非常的酷吧? 我們不需要寫一行代碼,就可以打開一個新界面。 我們僅僅從這個靜態單元格拖動到了新的 Scene上面,這就行了。(注意,當你點擊 Game 單元格時,PlayerDetailsViewController 中的代理方法 didSelectRowAtIndexPath 還是會被調用, 所以要確保你這裏的代碼不會和 segue 衝突)。

當然, 如果這個新界面不往回發送數據的話,那它一點用處也沒有, 所以我們必須爲它增加一個新的代理。 在 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;

這裏增加了一個新的 ivar, selectedIndex , 並且用 synthesize 聲明瞭這個屬性。

然後,在 viewDidLoad 底部增加這幾行:

selectedIndex = [games indexOfObject:self.game];

選中的遊戲的名稱,會被設置到 self.game 中。 這裏我們找到這個遊戲在 games 數組中的索引。 我們將用這個索引來設置單元格的選中狀態。 爲了能正常運行, 必須在視圖加載完成之前 self.game 賦值。 因爲我們是在調用者的 prepareForSegue 方法中給他賦值,這個方法會在 viewDidLoad 之前執行, 所以這就不成問題了。

Change cellForRowAtIndexPath to:

- (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];
}

在單元格被點擊時,我們首先反選了它。 這讓它從高亮的藍色變成常規的白色。 然後我們刪除之前選中的單元格的選中符號, 並且把它放到我們剛剛點擊的單元格上面。 最後, 我們把選中的遊戲的名稱傳遞給代理對象。

運行應用,測試一下這個。 點擊一個遊戲的名稱,它所在的單元格會得到一個選中符號。 點擊另一個遊戲的名稱, 選中符號就會移動到它上面。 這個界面應該在你點擊任何一行後馬上消失, 但是它沒有這樣, 因爲你還沒有綁定代理對象。

在 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;
	}
}

這和我們之前做的很像。 這次目標控制器是一個 Game Picker 界面。 記住這裏的代碼會在 GamePickerViewController 初始化之後, 和它的視圖被加載之前執行。

game 變量是一個新的變量:

@implementation PlayerDetailsViewController
{
	NSString *game;
}

我們用這個 ivar 來記錄選中的遊戲,所以我們可以稍後再把它存放到 Player 對象中。 我們應該給它一個默認值。 initWithCoder 方法是做這個事情的好地方。

- (id)initWithCoder:(NSCoder *)aDecoder
{
	if ((self = [super initWithCoder:aDecoder]))
	{
		NSLog(@"init PlayerDetailsViewController");
		game = @"Chess";
	}
	return self;
}

如果你以前用過 nib, 你應該會比較熟悉 initWithCoder 這個方法。 在 storyboard 中它還是同樣的功能。 initWithCoder,awakeFromNib 和 viewDidLoad 這些方法仍然會被使用。 你可以把 Storyboard 想象成一個很多 nib 的集合, 並且附帶了控制器間如何切換,和他們之間的關係的信息。 但是storyboard中的視圖和視圖控制器,還是以同樣的方式編碼和解碼。

修改 viewDidLoad 方法, 用來把遊戲的名稱顯示到單元格上:

- (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];
}

這個看起來很直觀, 我們把新選擇的遊戲的名稱賦值給 game 實例變量和單元格上面的 Label, 然後我們關閉了遊戲選擇界面。 因爲它是一個 push segue, 我們必須把它從導航欄的棧中彈出來關閉它。

我們的 done 方法,現在可以把選擇好的遊戲的名稱賦值給 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];
}

棒極了,我們現在有了一個功能齊全的遊戲選擇界面!

Choose Game screen with checkmark


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章