iOS开发教程:Storyboard全解析-第二部分
如果你想了解更多Storyboard的特性,那么你就來對了地方,下面我們就來接著上次的內容詳細講解Storyboard的使用方法。
在上一篇《iOS開發教程:Storyboard全解析-第一部分》中,我們介紹了如何使用storyboard來制作多種場景和如何將這些場景鏈接起來,我們還學習了如何自定義一個表格視圖。
接下來這部分,也是最后一部分,我們將講解聯線(segue),靜態單元格等內容,我們還將加入一個選手詳細內容頁面,和一個游戲選擇頁面。
我們接著上一次的內容制作,如果你還沒有上一次的源碼,請到我的Github空間下載,地址在這里。
?
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哦。
我們接下來在這個教程中還會用到幾次代理方法,下面我們來列一下為了完成一個連線,你需要做的幾件事情。
?
我們在這里必須使用代理,是因為根本沒有反向聯線這種東西,當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 *)tableViewdidSelectRowAtIndexPath:(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:selfdidAddPlayer:player]; } |
這需要我們引進Player的頭文件:
| #import "Player.h" |
這個完成方法會創建一個新的Player實例,并把它發送給代理器,由于目前代理器還沒有這個方法,所以我們需要在PlayerDetailsViewController的頭文件中修改如下代碼:
?
| @class Player;@protocol PlayerDetailsViewControllerDelegate <NSObject> - (void)playerDetailsViewControllerDidCancel:(PlayerDetailsViewController *)controller; - (void)playerDetailsViewController:(PlayerDetailsViewController *)controllerdidAddPlayer:(Player *)player; @end |
這個“Did Save”的方法的聲明沒有了,我們加入一個“didAddPlayer”方法。
?
下面我們需要在執行文件中加入執行的方法,打開PlayersViewController.m,加入:
| ? - (void)playerDetailsViewController:(PlayerDetailsViewController *)controllerdidAddPlayer:(Player *)player {[self.players addObject:player];NSIndexPath *indexPath =[NSIndexPath indexPathForRow:[self.players count] - 1inSection: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 *)tableViewnumberOfRowsInSection:(NSInteger)section {return [games count]; }- (UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath *)indexPath {UITableViewCell *cell = [tableViewdequeueReusableCellWithIdentifier:@"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 *)controllerdidSelectGame:(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 = [tableViewdequeueReusableCellWithIdentifier:@"GameCell"];cell.textLabel.text = [games objectAtIndex:indexPath.row];if (indexPath.row == selectedIndex)cell.accessoryType =UITableViewCellAccessoryCheckmark;elsecell.accessoryType = UITableViewCellAccessoryNone;return cell; } |
這個方法會在選中的項目的右邊加上一個選中的對勾。
?
將 didSelectRowAtIndexPath 修改為:
| - (void)tableView:(UITableView *)tableViewdidSelectRowAtIndexPath:(NSIndexPath *)indexPath {[tableView deselectRowAtIndexPath:indexPath animated:YES];if (selectedIndex != NSNotFound){UITableViewCell *cell = [tableViewcellForRowAtIndexPath:[NSIndexPathindexPathForRow: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:selfdidSelectGame: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 *)controllerdidSelectGame:(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,到這里我們就完成了游戲選擇器的場景,不錯吧。
?
接下來看些什么:
?
這是我制作完成的例子程序源碼,歡迎下載。GitHub不知道出了什么問題,只好上傳到網盤了,連接如下。
?
歡迎關注我的圍脖: @Oratis
在知乎和豆瓣上,我的名字也是Oratis
?
我會把之后發表的教程分享到這些社交網絡中。
?
如果你有任何問題歡迎寫信給我,我的郵箱地址是: oratis@appkon.com
總結
以上是生活随笔為你收集整理的iOS开发教程:Storyboard全解析-第二部分的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 应用跳转到AppStore指定关键字搜索
- 下一篇: 从iOS应用中,启动一个Unity Ap