感謝原文做者分享編程
故事版(Storyboard)是一個可以節省你不少設計手機App界面時間的新特性,下面,爲了簡明的說明Storyboard的效果,我貼上本教程所完成的Storyboard的截圖:小程序
如今,你就能夠清楚的看到這個應用到底是幹些什麼的,也能夠清楚的看到其中的各類關係,這就是Storyboard的強大之處了。若是你要製做一個頁面不少很複雜的App,Storyboard能夠幫助你解決寫不少重複的跳轉方法的麻煩,節省不少時間,以便你可以徹底的專一於核心功能的實現上。數組
開始app
首先啓動Xcode,新建一個工程,咱們在這裏使用Single View App Template,這個模板會提供一個類和一個Storyboard,免去咱們本身建立的麻煩。框架
建立完成以後,Xcode的界面大概是這樣的:編輯器
這個新的工程由兩個類:AppDelegate和ViewController以及一個Storyboard組成(若是你選擇了兩個設備會有兩個Storyboard),注意這個項目沒有xib文件,讓咱們首先看看Storyboard是什麼樣的,雙擊Storyboard打開他:ide
Storyboard的樣子和工做方式都和Interface Builder(如下簡稱爲IB)像極了,你能夠從左下方的控件庫中拖動控件到你的View之中而且組織他們的排放順序,惟一不一樣的地方就是,Storyboard不止是包含一個視圖控件,而是全部的視圖控件以及他們之間的關係。函數
Storyboard對一個視圖的官方術語是一個場景,可是一個場景其實就是一個ViewController,在iPhone中一次只可以展現一個場景,而在iPad中一次能夠展現多個場景,好比Mail應用程序。ui
經過嘗試添加一些控件,你能夠感覺一下Storyboard的工做方式。this
這個是數據顯示器,顯示全部場景及其控件的結構。
在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類的名字是什麼,不然他將沒法找到。
本教程中的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。
I
請注意:要把這個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按鈕的時候消失,不過這只是一個練習,不要太較真哦親