Storyboard 解析
?
故事版(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文件,看看那上面是怎么說的:
| #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 = [[tabBarControllerviewControllers] 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 *)tableViewnumberOfRowsInSection:(NSInteger)section {return [self.players count]; } |
真正起作用的代碼在cellForRowAtIndexPath方法里,默認的模板是如下這樣的:
| ? - (UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath *)indexPath {static NSString *CellIdentifier = @"Cell";UITableViewCell *cell = [tableViewdequeueReusableCellWithIdentifier:CellIdentifier];if (cell == nil) {cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefaultreuseIdentifier:CellIdentifier];}// Configure the cell...return cell; } |
無疑這就是以前設置一個表格視圖的方法,不過現在已經革新了,把這些代碼修改如下:
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {UITableViewCell *cell = [tableViewdequeueReusableCellWithIdentifier:@"PlayerCell"];Player *player = [self.players objectAtIndex:indexPath.row];cell.textLabel.text = player.name;cell.detailTextLabel.text = player.game;return cell; } |
這看上去簡單多了,為了新建單元格,你只需使用如下代碼:
| UITableViewCell *cell = [tableViewdequeueReusableCellWithIdentifier:@"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 *)tableViewcellForRowAtIndexPath:(NSIndexPath *)indexPath {UITableViewCell *cell = [tableViewdequeueReusableCellWithIdentifier:@"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 *)[tableViewdequeueReusableCellWithIdentifier:@"PlayerCell"];Player *player = [self.players objectAtIndex:indexPath.row];cell.nameLabel.text = player.name;cell.gameLabel.text = player.game;cell.ratingImageView.image = [selfimageForRating: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 解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python编程:从入门到实践pdf
- 下一篇: 证券市场基础知识(二)——股票、债券、基