原貼地址:http://www.iteye.com/topic/1122984
如果你想了解更多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 如下:
- @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,到這裏我們就完成了遊戲選擇器的場景,不錯吧。