在前面幾部分中,主屏幕只能展示一個汽車對象的信息。而在實際IOS中,一次顯示多條數據並實現滾動查看是十分常見的,例如通訊錄、音樂以及其他多個以表格樣式展示數據的應用程序。
表視圖是開發者的工具包中至關重要的控件之一,當設計和創建那些運行在屏幕空間不大的iPhone/iPad設備上的應用陳旭時,該視圖的作用尤為重要。與UINavigationController結合使用時,表視圖控件能建議地展示有著層次結構的數據,輕松實現導航。表視圖允許用戶便捷地訪問對象概要列表。本節將介紹有關表視圖的一些基本知識。
表視圖介紹 3類與2協議 UITableViewController是主視圖控制器,擁有很少幾個有關視圖的屬性,並且只有一個方法。大多數工作由兩個控制器所遵從的協議來實現。通過實現這些協議的方法,可以顯示表視圖的單元格,在某些情況下,實現第用戶交互進行響應。 UITableViewDataSouce提供的是從對象獲取的數據詳情,這些數據會在與表中的每個表格段外和表格行對應的每一個單元格中進行展示。此外提供了多個可選的方法,用於實現單元格的添加、刪除和排序。 UITableViewDelegate支持表視圖的多個行為。利用UITableViewDelegate,可以實現任何操作:從配置某個單元格的高度或表頭視圖,以及支持編輯和突出強調功能。 UITableView實現所有的功能,能夠配置並顯示表格視圖的各個部分。它擁有一些額外的方法和屬性,能夠實現添加、移動和刪除單元格;更新數據,手動實現視圖內容的滾動;訪問視圖的其他部分;配置索引等。當然,其中的某些功能,最好由視圖數據源(UITableDataSouce)和委托(UITableViewDelegate)來實現。 UITableViewCell表示在表視圖中顯示的單行。通過“標題和副標題,添加圖片”的不同配置,能形成4種不同的、系統自帶的單元格樣式。此外,也可以自定義單元格的樣式。 表視圖的常見行為 設置——對表視圖的表頭視圖、表尾視圖、表格段、每個表格段的表頭和/或表尾、每組中單元格的個數進行設置。 顯示——顯示可見的表格 滾動——計算出大暖閣何時進入屏幕並負責加載它們。表視圖管理單元格的創建,以及單元格的重用,它會創建緩存池,存儲已創建的單元格,並負責權衡如何為賦予了新的數據之後、即將滾入屏幕的單元格的配置樣式。 選中——更新單元格的選中狀態或者負責更新表視圖配置的單元格。根據選中狀態來調用相應的委托方法。 階段I:學習一下表視圖的基本用法 創建空白文件 首先我們先創建一個Empty Application的模板(當然xcode6以後就沒有這個了),但是沒關系,我們正常創建一個 Single View Application模板,然後將 ViewController.m、ViewController.h和Main.storyboard刪除。
(方法查自於http://www.cocoachina.com/bbs/read.php?tid=231701)
這樣就創建了一個Empty Application了。
接下來在xcode中選擇file|new,添加一個iphone故事板文件,選中user interface類別中的storyboard,添加到文件中,將.plist將Main storyboard file base name 那項刪掉中的Main改成剛才添加的那個故事面版文件。
我們得到了一個什麼都沒有的故事面板文件,這時候我們將一個表視圖控制器(Table View Controller)拖入到故事面板文件中,然後點擊剛才放入的故事面板,將 Is Initial View Controller 選中
好了,創建完了
現在,最簡單的詩經就是創建一些預設的單元格了
選擇主故事版(VIew Controller)並選擇Attributes檢查器 在Attributes檢查器的頂端的Table View部分,將Content下拉菜單選為Static Cells(靜態單元格),可以看到,故事面板表視圖圖像由一個變成了三個。如下圖(嘿嘿嘿,升級版動態圖)運行後,雖然看起來沒變,但是點擊上面三行表格時,表格會變成灰色表示選中,而之前是不行的
創建單元格有時候我們想要創建預先定義的單元格。但更常見的情況是,我們想要顯示數量不定、數量可變的數據項列表,因此我們就需要提前知道其內容。上面我們創建的表視圖是使用靜態單元格。在表格中,它們的表現類似於靜態視圖。但有時候,我們不知道需要顯示多少單元格,以及如何來顯示它們。此時,我們就需要使用動態原型。
當然,使用什麼類型的單元格取決於我們要做什麼,像setting顯然我們是知道需要多少單元格的就使用靜態單元格,而類似於備忘錄的,則是使用動態原型。
下面我們將進行動態原型的演練 將表格類型改回原來的動態原型,將多出來的兩個單元格直接刪掉就行了;選中Table View Cell,將Identifiler設置為MyCell
如果沒有設置為MyCell(其他名字也行,總之要有名字),會有如下警告
這是在告訴我們:“原型單元格(prototype cell)沒有設置重用標識符。重用標識符(reuse identifier)能讓表視圖控制器判定創建和重用哪一種類型的單元格原型
現在我們需要編寫代碼告訴表視圖需要創建多少單元格,並需要給這些單元格填充數據,因此,我們需要一個UITableViewDataSource。
創建步驟:(跟以前創建的差不多,不過有點變化)
創建新文件,選擇Cocoa Touch類別,選擇Objective-C類返回故事版,選中表視圖控制器(再次說一下就是Table View Controller),並且使用Identity檢查器將該類設置為MyTableViewController
在MyTableViewController.m中會有警告,就是文件中有#warning,這個特殊的標記是要告訴xcode有些事需要處理。警告是要告訴我們還需要寫一寫代碼。
分行和組每個警告都告訴我們一些知識,有關表視圖如何組織數據的方式。表視圖的數據組(表格段)可以為0個或多個,每個表格段可以擁有0行或者多行的數據。個數從0開始計,看上去很奇怪,但是在表視圖中,全控的數據集或僅僅是空的表格段都是有可能的
下面我們通過代碼告訴表視圖數據段有多少,以及每段數據含有多少行(其實從方法名稱上就能大致知道怎麼用了)
將MyTableViewController.m中的兩個方法替換為如下代碼//這個方法是告訴表視圖一共有多少數據段(該方法是可選的,如果沒有提供該方法,則默認1)
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
//warning Incomplete implementation, return the number of sections
return 1;
}
//這個方法說明指定的數據段含有多少個數據項,這裡我們弄5個
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
//warning Incomplete implementation, return the number of rows
return 5;
}
在剛替換的兩個方法下面的tableView:cellForRowAtIndexPath:方法中,將CellIndentifier的值由@"Cell"
改為@"MyCell
。這告訴表視圖創建的新的單元格時應用哪個原型。
再次運行,這時候會有5個可以單擊的單元格
生成單元格和索引路徑(Index Path)首先,我們需要知道表視圖中的Index Path是什麼。表視圖數據顯示為一個或多個組。每個行組被稱為section(表格段)。表中的任何行可以根據它的組和行的數字,進行識別、予以區分。這個組合被稱為index Path(索引路徑),(我覺得可以簡單的認為是一個動態的二維數組)
在表視圖中,所以路徑使用的是NSIndexPath類中的一個更通用、專門針對表視圖的版本來表示索引路徑。它由兩個整數組成:第一個是表格段編號,第二個是行號。因為索引路徑是對象,所以可以發送消息或使用點表示法來訪問其部件。表視圖使用一組特殊訪問器,section和row讓代碼更容易讀且更可維護
如果在表中有個NSIndexPath名為cellIndexPath,那麼可以使用cellIndexPath.section來獲取它的表格段標號,用cellIndexPath.row來訪問他的行。
代碼如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell" forIndexPath:indexPath];
NSString *myLabel = [NSString stringWithFormat:@"@Section: %ld Row: %ld",(long)indexPath.section,(long)indexPath.row];
cell.textLabel.text = myLabel;
return cell;
}
運行結果:
添加表格段 現在我們要像表格中添加表格段。要實現這一點,僅僅需要將numberOfSectionsInTableView:
的返回值從1改為2,運行這個程序,則會有兩個含有5分元素的表格段,但是它們沒有表格段的表頭視圖。
添加表格段的表頭視圖,只需要在numberOfSectionsInTableView:
方法的下方插入以下代碼
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [NSString stringWithFormat:@"Section: %ld",(long)section];
}
效果如下圖
可以試著添加更多的表格段或再每個表格段中添加更多的單元格。即使添加的數量非常大,表視圖還是能工作。部分原因是,表視圖僅僅會保留那些處於可見的單元格,以及一些額外的單元格行,從而實現快速的滾動。如果創建一個含有數千數據的表格,內存中一次只保留幾十個數據。表視圖創建他所需要的單元格,並回收已經不在屏幕上的單元格。這正是重用標識符的用處。
階段II:替換Add/View場景PS可以到ArnoldYUV ,我的git上下載代碼
替換表視圖控制器首先,將Add/View場景替換為一個基於表視圖控制器的場景:
打開故事面板,拖入一個表視圖控制器,並將其放到已有的Edit/View場景的上方; 在我們的項目中添加兩個新的Objective-C類:CarTableViewController繼承自UITableViewController,CarTableViewCell繼承自UITableViewCell,這裡就不貼圖了; 在故事面板中,將第一步添加的表視圖控制器的類設置為CarTableViewController,並且將表視圖的原型單元格的類設置為CarTableViewCell。保持原型單元格被選中,選擇屬性並將重用標識符設置為CarCell; 將Car對象的一個屬性添加到汽車單元格中。這個屬性用於單元格中要顯示的數據,並且最終決定查看和編輯哪輛汽車。打開CarTableViewCell.h並添加如下代碼:#import <UIKit/UIKit.h>
@class Car;
@interface CarTableViewCell : UITableViewCell
@property (strong, nonatomic) Car *myCar;
@end
將Car.h導入CarTableViewCell.m文件中
打開CarTableViewController.m並添加如下代碼,除了導入汽車模型類和汽車表視圖單元格外,還需聲明一個汽車數據數組,正如我們在ViewController.m中所實現的操作:
#import "CarTableViewController.h"
#import "Car.h"
#import "CarTableVIewCell.h"
@interface CarTableViewController ()
@end
@implementation CarTableViewController{
NSMutableArray *arrayOfCars;//這是用來存車的數組
}
將VIewController.m中的newCar:方法復制到CarTableController.m文件的底部,在@end的上面。然後刪除updateLabel:withBaseString:andCount:調用
在CarTableViewController.m中,修改viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
arrayOfCars = [NSMutableArray new];
[self newCar:nil];
}
將表格段編號修改為1,將總的行數修改為arrayOfCars數組的大小:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
//根據有多少量車就設置多少單元格
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [arrayOfCars count];
}
在tableView:cellForRowAtIndexPath:
中,確保靜態的CellIdentifier正確設置。代碼如下
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"CarCell";
//.. .. ..我忘了之前啥樣子的.. .. .. 關鍵是要加上面這一句
//暫時這些 還有
}
切換到故事版,並改變導航控制器的指向
運行程序後。表視圖控制器現在是Add/View場景。在繼續進行之前,將導航欄的標題設置為CarValet,也可以從ViewController.m中復制本地化代碼
添加汽車查看單元格我們現在已經有了一輛汽車的數據了,以及一個現實這輛汽車的單元格。但是我們沒有擁有視圖元素來顯示該數據。下一步就是修改原型單元格,從而顯示汽車數據。步驟如下:
向Car對象中添加類為NSDate(很容易錯寫成NSData要注意)的屬性dateCreated。在基類的初始化方法中將其設置為[NSDate date]//在Car.h中
@property NSDate* dateCreated;//新加
//在Car.m中
- (id)init {
self = [super init];
if (self != nil) {
_year = kModelTYear;
_fuleAmount = 0.0f;
_dateCreated = [NSDate date];//新加
}
return self;
}
- (id)initWithMake:(NSString *)make
model:(NSString *)model
year:(int)year
fuelAmount:(float)fuelAmount {
self = [super init];
if (self !=nil) {
_make = [make copy];
_model = [model copy];
_year = year;
_fuleAmount = fuelAmount;
_dateCreated = [NSDate date];//新加
}
return self;
}
在故事板上,打開汽車表視圖的單元格的Attributes檢查器,並且將其設置為RightDetail樣式(其實上面已經有圖這麼弄了),這是表格的樣式會改變,會在坐標以黑色顯示標題,並且在右邊以淺灰色顯示詳情。將詳情的字號設置為12。
向CarTableViewCell中添加一個公有方法,代碼如下,注意要在.h文件中聲明
//.h文件 增加
- (void)configureCell;
//.m文件
- (void)configureCell {
NSString *make = (self.myCar.make == nil) ? @"Unknown" : self.myCar.make;//1 汽車的make或model可能為nil,所以將其設置為默認值
NSString *model = (self.myCar.model == nil) ? @"Unknown" : self.myCar.model;
self.textLabel.text = [NSString stringWithFormat:@"%d %@ %@",self.myCar.year,make,model];//2 將主編前設置為汽車的year、make和model
NSString *dateStr = [NSDateFormatter localizedStringFromDate:self.myCar.dateCreated
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterShortStyle];//3 獲得本地化版本的創建日期,日期樣式應盡可能簡短
NSLog(@"%@", self.myCar.dateCreated);
self.detailTextLabel.text = dateStr;//4 將段版本的創建日期設置到詳細信息文本去中
}
在CarTableViewController.m的tableView:cellForRowAtIndexPath:方法中添加代碼,為單元格設置數據。然後讓單元格更新自己
//上面說少一部分的那段代碼完整的在這裡
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"CarCell";
CarTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.myCar = arrayOfCars[indexPath.row];
[cell configureCell];
return cell;
}
//注意必須修改我們所創建的單元格的類,因為只有這樣操作,它才能知道myCar屬性和configureCell方法
添加汽車
我們已經有了一個添加汽車的方法(就是表示圖文件那個newcar的方法)。我們只需要通過一個按鈕就可以實現對他的調用了。
打開故事板,將一個bar button元素拖入到Add/View場景的導航欄(建議放右邊)選中剛才放進去的按鈕,如下操作,就能變成“➕”了
由動態圖就是方便,注意是右擊,這樣就為按鈕添加方法了
在newCar:方法的末尾添加如下代碼讓表視圖重載自己(不然你點擊後會發現程序貌似沒有變,其實已經變了,只是沒有刷新)
[self.tableView reloadData];
如果能讓新車帶動畫的進入表格頂部,應該會更好一點。對newCar方法進行修改,代碼如下:
- (IBAction)newCar:(id)sender {
Car *newCar = [Car new];
[arrayOfCars insertObject:newCar atIndex:0];//1 將汽車插入到數組的前邊
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];//2 創建一個NSindexPath對象來指定新單元格的位置——他的section和row
[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];//3 讓表視圖在新的索引路徑上插入一個對象。這回事表視圖調用數據源來查找第0表格段第0行的數據——也就是數組的第一個元素。因為數組已經被更新,所以新的單元格會被返回
//[self.tableView reloadData];
}
(運行如圖:PS看前面增加的效果就行了……後面的是後續的內容)
刪除汽車能夠創建汽車,當然也就可以刪除汽車了。其實,在我們添加CarTableViewController類的時候,一個簡單的刪除方法所需要的大部分代碼已經創建好了。我們只需要將他的注釋刪除掉就可以了
在CarTableViewController.m修改代碼如下:
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
return YES;//這裡如果返回的是no那麼就是不能編輯的包括刪除,假設我們寫的是return (indexPath.row%2!=0);那麼0,2,4這些行數就不能進行操作了,下面給出一個不能刪除的模擬運行圖
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
[arrayOfCars removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
} //else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
//}
}
(這是之前截的圖不要在意內容,在意要展示的效果就行啦)
掃動是很好的,但是如果是新手的話,可能不知道掃動這個手勢。所以我們最好是提供一個用戶界面元素用於編輯會好一些。為此,通過以下步驟添加一個編輯按鈕:
打開故事面板。病將兩個bar button item拖動視圖元素列表中First Responder下面的表視圖控制器浏覽器中
為剛才添加的兩個工具欄按鈕一個命名為Edit,另一個命名為Done
打開Assistant編譯器,確保打開的是CarTableViewController.h,分別為每個按鈕添加一個屬性,editButton指向Edit,doneButton指向Done 為兩個按鈕添加一個action,命名為editTableView,兩個按鈕都指向這個action 將下面的代碼添加到viewDidLoad方法的末尾:self.navigationItem.leftBarButtonItem = self.editButton;//為導航欄添加左按鈕
使用下面代碼填充editTableView:方法
- (IBAction)editTableView:(id)sender {
BOOL startEdit = (sender == self.editButton); //1 如果發送這條消息的是編輯按鈕,那麼開始編輯
UIBarButtonItem *nextButton = (startEdit) ? self.doneButton :self.editButton; //2 下一個要顯示的按鈕是當前沒有顯示的那個,就是交替出現done和edit
[self.navigationItem setLeftBarButtonItem:nextButton animated:YES];//3為新的導航欄按鈕添加動畫
[self.tableView setEditing:startEdit animated:YES];//4讓表視圖動畫過度到編輯或非編輯狀態
}
運行如圖:
階段III在本節中,我們將添加一些場景用來查看和編輯汽車。
添加查看汽車場景在故事版中完成創建汽車詳情屏幕的大多數可視化工作:
拖入一個表視圖控制器,就把他放到新汽車場景的左側吧 選擇表視圖並將內容設置為static cells,將樣式設置為Grouped。當修改樣式時,會看到表格段數目的設置。將表格設置為三個表格段基於每個組要顯示的數據數目來修改其中的單元格數目:
選中第一個分組,然後Attributes檢查器會發生變化,允許修改行數、表頭和表尾。將行數保持為3,將表頭設置為Make、Model&year。這些表頭文字要添加到故事板的分組中
將帶有一個單元格的第二個分組的表頭設置為Fuel
將第三個分組的表頭設置為Date PARKED
對每個單元格,將類型設置為Basic,將內容設置到它所顯示的數據元素的名稱。例如,第一個分組的三個單元格分別為Make,Model,Year
最後的結果圖如下
現在看看這個視圖:
從原型汽車單元格拖拽一個Push Selection Segue拖到查看汽車表視圖控制器。確保不要選中附屬segue,因為它們由單元格附屬控件觸發,而不是選擇整個單元格時觸發。此外,確保是從單元格拖拽而不是其中的其他內容 選中剛才出啊年segue,將標識符設置為ViewSegue 將汽車詳情場景的導航標題欄設置為View Car結果圖如下:
運行結果如下:
用數據填充查看汽車場景要為查看汽車場景填入數據,需要為這個場景的視圖控制器制定一個自定義類,步驟如下:
添加繼承UITableViewController類的ViewCarTableViewController 添加下面代碼到ViewCarTableViewController.h中:@class Car;
@property Car *myCar
打開故事版,選中靜態表視圖控制器也就是我們剛才創建的顯示汽車詳情的表視圖控制器,並將其類設置為ViewCarTableViewController
打開Assistant編輯器,並確保他所顯示的是ViewCarTableViewController.h。把每個標簽拖拽到文件中,命名為makeLabel、modelLabel、yearLabel、fuelLabel、dateLabel和timeLabel。確保這些屬性的類型為UITable。這時候ViewCarTableViewController.h代碼應該如下:
#import <UIKit/UIKit.h>
@class Car;
@interface ViewCarTableViewController : UITableViewController
@property (weak, nonatomic) IBOutlet UILabel *makeLabel;
@property (weak, nonatomic) IBOutlet UILabel *modelLabel;
@property (weak, nonatomic) IBOutlet UILabel *yearLabel;
@property (weak, nonatomic) IBOutlet UILabel *fuelLabel;
@property (weak, nonatomic) IBOutlet UILabel *dateLabel;
@property (weak, nonatomic) IBOutlet UILabel *timeLabel;
@property Car *myCar;
@end
在編輯器中打開ViewCarTableViewController.m並導入Car.h。因為這個表格顯示靜態單元格,不需要任何數據源協議方法,因此將他們刪除。也可以在viewDidLoad方法中設置一些動態內容。代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
dataUpdated = NO;
self.myCar = [self.delegate carToView];
self.makeLabel.text = (self.myCar.make == nil) ? @"Unknown" : self.myCar.make;
self.modelLabel.text = (self.myCar.model == nil) ? @"Unknown" : self.myCar.model;
self.yearLabel.text = [NSString stringWithFormat:@"%d",self.myCar.year];
self.fuelLabel.text = [NSString stringWithFormat:@"%0.2f",self.myCar.fuelAmount];
self.dateLabel.text = [NSDateFormatter localizedStringFromDate:self.myCar.dateCreated
dateStyle:NSDateFormatterMediumStyle
timeStyle:NSDateFormatterNoStyle];
self.timeLabel.text = [NSDateFormatter localizedStringFromDate:self.myCar.dateCreated
dateStyle:NSDateFormatterNoStyle
timeStyle:NSDateFormatterMediumStyle];
// Uncomment the folloWing line to preserve selection between presentations.
// self.cleaXmlRss/ target=_blank class=infotextkey>XmlRss/ target=_blank class=infotextkey>RsselectionOnViewWillAppear = NO;
// Uncomment the folloWing line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
將ViewCarTableViewController.h導入CarTableViewController.m中,並在viewDidLoad方法下添加如下方法:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"ViewSegue"]) {
ViewCarTableViewController *nextController;
nextController = segue.destinationViewController;
// NSInteger index = [self.tableView indexPathForSelectedRow].row;
// nextController.myCar = arrayOfCars[index];
nextController.delegate = self;
}
}
為ViewController類創建類似的prepareForSeguesender:方法。在此處,為傳進來的ViewCarTableViewController設置Car對象
運行程序,穿件一些汽車,並單機其中的一些。新的查看汽車場景會打開,我們能夠多動這個表格,並且旋轉也能正確處理。實際上,我們創建的新場景都能處理旋轉,改變他們的單元格尺寸,並根據需要來支持滾動
當選中一輛不同的汽車時,這個程序顯示含有正確數據的新增加汽車視圖屏幕、當然這些詳情屏幕的去呗只有Date Packed文本區。現在該添加make、model和year的編輯控件了
對了,兩個表視圖控制器的底部可能會有一個白條(忘了前面有沒有取消掉了)。這是之前章節創建的工具欄。可以自己嘗試的去刪除他。
編輯數據汽車的大部分數據都可編輯。我們需要提供場景來編輯汽車的不同屬性。首先是make和model(這倆一樣)。所有元素都基於文本,因此他們都可以使用同一類場景,步驟如下:
將一個UIViewController拖動到查看汽車場景的右側 在接近頂部的地方添加一個UINavigationBar,並且將首尾約束都設置為0,頂部約束標准距離 往導航欄的每一段拖入一個工具欄按鈕,將左邊的一個設置為Cancel按鈕,將右邊的一個設置為Done按鈕 拖入一個標簽,將其約束設置為距離導航元素的底部20點,首尾兩邊為0 拖入一個文本框,並將其約束設置為距離標簽下面為系統距離,然後與首尾兩邊為0 保持文本框被選中,使用Attributes檢查器,將清除按鈕設置為Appear While Editing。新的控制器用於編輯汽車的品牌或型號。這意味著,在視圖顯示之前,標題、標簽和文本框數據需要被設置好。有兩種方法實現:查看汽車場景要麼可在prepareForSeguesender:裡面設置這些屬性,要麼可以使用協議。使用協議是更好的方法,因為具有更高的靈活性和可維護性,另外還意味著可以在其他項目中重用這些元素
到此為止,當改變當前屏幕時,可以沿著視圖層次向上或鄉下導航,或者切換到一個新的標簽頁或視圖集合。在一個層次結構中,我們從所有汽車的概覽,到達一輛汽車的詳情。隨著層次深入,用戶可以看到視圖從右側滑入並且當向上返回時,從左側消失
有事我們想要顯示某些視圖層次結構之外的東西。編輯汽車的特定屬性是個示例。編輯並不是修改某件東西的能力;我們還提供取消修改的機會,即使用戶已經輸入了新的值
這樣的操作是不同的,這是向用戶表明他或她並不只向前進一步操作的好方式。使用一個模態屏幕實現該要求,這能迫使用戶做出選擇。用戶做出修改後既可以接受也可以取消。用戶現在不能浏覽其他地方的應用程序,直到做出選擇之後。模態屏幕,設置切換的視覺效果也與通常不同。模態屏幕中默認切換效果是從底部和下面滑入,結束時消失
在iOS中,可以將任何場景(或視圖控制器)顯示為模式。如何實現轉場有不同選項。正如下面我們將看到的,可以使用模態segue或對當前導航控制器的不同調用
而當用戶已經完成編輯時,觸摸Done或Cancel的一個,回到原來的顯示模態屏幕的場景。當使用segue顯示模態屏幕時,返回叫回退(unWinding)
步驟如下,創建交換數據的協議:
創建名為MakeModelEditViewController的一個視圖控制器子類,並將其設置為剛創建的場景的類 確保新的make/model的編輯場景在故事版中被選中,並且.h文件在輔助視圖中被打開。將標簽拖出屬性命名為editLabel,將文本框拖出屬性命名為editField 將導航欄拖出屬性,命名為myNavigationItem 將Cancel和Done按鈕拖拽,創建action 名為:editCancel和editDone 添加名為MakeModelEditProtocol的協議並將文件的內容設置為下面的代碼。該協議中的每個方法被用於創建make/model編輯場景的不同部分,除了最後一個,它用來將編輯後的值發送回委托對象:#import <Foundation/Foundation.h>
@protocol MakeModelEditProtocol <NSObject>
- (NSString*)titleText;
- (NSString*)editLabelText;
- (NSString*)editFieldText;
- (NSString*)editFieldPlaceholderText;
- (void)editDone:(NSString*)textFieldValue;
@end
將這個協議導入到MakeModelEditViewController.h中,並添加如屬性:
#import "MakeModelEditProtocol.h"
@property (weak, nonatomic) id <MakeModelEditProtocol> delegate;
使用協議方法初始化標題、標簽和文本編輯框。打開MakeViewCarTableViewController.m並用下面的代碼替換viewDidLoad,將用戶界面中的每個元素設置為委托返回的內容:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.myNavigationItem.title = [self.delegate titleText];
self.editLabel.text = [self.delegate editLabelText];
self.editField.text = [self.delegate editFieldText];
self.editField.placeholder = [self.delegate editFieldPlaceholderText];
[self.editField becomeFirstResponder];
}
Make/Model控制器所需要的其他方法只有cancelTouched:和editTouched:。這些方法被切換回那個打開編輯器的場景。此外,如果用戶觸摸Done,當前的文本框值會被發送給委托。我們需要關閉一個為模態切花而打開的控制器,並且同時回退segue。可以使用UIViewController的方法dismissViewControllerAnimated:completion:來實現這一點。用下面的代碼替換兩個IBAction:
- (IBAction)editCancel:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)editDone:(id)sender {
[self.delegate editDone:self.editField.text];
[self dismissViewControllerAnimated:YES completion:nil];
}
將協議導入ViewCarTableViewController.h並且趕工在@interface聲明行之後添加下面代碼:
<MakeModelEditProtocol>
上面代碼表示ViewCarTableViewController同意遵照該協議
將這些協議添加到ViewCarTableViewController.m的底部,剛好在@end的上面- (NSString*)titleText {
}
- (NSString*)editLabelText {
}
- (NSString*)editFieldText {
}
- (NSString*)editFieldPlaceholderText {
}
讓查看汽車控制器成為MakeModelEditProtocol委托
make/model編輯器已經能夠編輯一些內容並讓委托得知結果。下一步就是准備委托,在當前這種情況下,也就是VIewCarTableViewController。這個不尋常的部分用於跟蹤當前正在被編輯的是哪個數據字段
make/model編輯器根本不在乎編輯的是什麼。他可以編輯任何文本元素。所有上下文都是由delegate設置的,這意味著查看汽車控制器需要跟蹤哪個輸入框正被編輯
通過下面步驟完成添加汽車品牌和型號的編輯工能:
修改ViewCarTableViewController.m的頂部,並通過添加如下代碼來添加一個狀態白能量和一些狀態值常量:#define kCurrentEditMake 0
#define kCurrentEditModel 1
@interface ViewCarTableViewController () {
NSInteger currentEdutType;
}
在故事板中,添加從品牌單元格到make/model編輯場景的模態選擇segue(不是輔助動作)。選中這個色鬼並且將標識符設置為MakeEditSegue。當設置標識符時,確保風格為Model。使用型號單元格重復這個動作,並且將標識符設置為ModelEditSegue
使用prepareForSeguesender:創建Make/Model編輯控制器。使用該方法設置正在編輯的是哪個數據字段。記得導入編輯器的頭文件,在當前這種情況下,是MakeModelEditViewController.h。在ViewCarTableViewController.m
中,代碼如下:
#import "MakeModelEditViewController.h"
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"MakeEditSegue"]) {
MakeModelEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
currentEdutType = kCurrentEditMake;
} else if ([segue.identifier isEqualToString:@"ModelEditSegue"]) {
MakeModelEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
currentEdutType = kCurrentEditModel;
}
}
為make/model編輯場景添加協議方法的內容。每個方法需要確定正在被編輯的是哪個字段,並返回正確的字符串;他們看上去基本相同。有很多方法可以實現這種行為。在當前這種情況下,使用switch語句來提供添加更多字段的靈活性,代碼如下:
- (NSString*)titleText {
NSString *titleString = @"";
switch (currentEdutType) {
case kCurrentEditMake:
titleString = @"Make";
break;
case kCurrentEditModel:
titleString = @"Model";
break;
default:
break;
}
return titleString;
}
- (NSString*)editLabelText {
NSString *titleString = @"";
switch (currentEdutType) {
case kCurrentEditMake:
titleString = @"Enter the Make:";
break;
case kCurrentEditModel:
titleString = @"Enter the Model:";
break;
default:
break;
}
return titleString;
}
- (NSString*)editFieldText {
NSString *titleString = @"";
switch (currentEdutType) {
case kCurrentEditMake:
titleString = self.myCar.make;
break;
case kCurrentEditModel:
titleString = self.myCar.model;
break;
default:
break;
}
return titleString;
}
- (NSString*)editFieldPlaceholderText {
NSString *titleString = @"";
switch (currentEdutType) {
case kCurrentEditMake:
titleString = @"Car Make";
break;
case kCurrentEditModel:
titleString = @"Car Model";
break;
default:
break;
}
return titleString;
}
使用從editDone:協議方法獲得的值來更新文本框,注意,用戶可能點擊Cancel從而不更新值,點擊Done的時候也可能不會更新(一進去就點Done),代碼如下:
- (void)editDone:(NSString*)textFieldValue {
if(textFieldValue != nil && [textFieldValue length] > 0) { //1 僅當有新的文本時才更新。如果文本框一開始根據editFieldText設置為nil,而且現在仍然是nil,那麼不做任何改變。它也可以是一個空字符串
switch (currentEdutType) {
case kCurrentEditMake:
if (self.myCar.make == nil || !([self.myCar.make isEqualToString:textFieldValue])) { //2 文本框中有一些文字,如果當前沒有值或者新值與舊值不同,則更新
self.myCar.make = textFieldValue; //3 如果有更新,則修改Car對象
self.makeLabel.text = textFieldValue; // 4 在汽車視圖中修改標簽
dataUpdated = YES;
}
break;
case kCurrentEditModel:
if (self.myCar.model == nil || !([self.myCar.model isEqualToString:textFieldValue])) {
self.myCar.model = textFieldValue;
self.modelLabel.text = textFieldValue;
dataUpdated = YES;
}
default:
break;
}
}
}
運行結果如下:
我們發現在根目錄中並沒有更新,這就是下面要解決的問題
添加ViewCarProtocol協議不存在從查看汽車控制器連接回汽車表格控制器的東西。唯一的通信是在CarTableViewController為進入的汽車視圖設置myCar時,發生在prepareForSegue:sender:方法中。我們需要一個協議來讓兩個視圖控制器能夠通信
這個協議需要兩條消息:一條用於設置要查看哪輛汽車,還有一條用於在數據發生變化時通知委托對象。最後一部分意味著查看汽車控制器必須跟蹤是否有變化。
在ViewCarTableViewController.m中,添加另一個狀態變量,類型為BOOL,位置在currentEditTyppe的下面。將他命名為dataUpdated。在viewDidload中,將dataUpdated初始化為NO,剛好在調用super代碼的下面 當在editDone:方法中更新self.myCar的任何時候將dataUpdated設置為YES 剛好在ViewCarTableViewController的下面,添加新的協議,名為ViewCarProtocol,並添加如下代碼:#import <Foundation/Foundation.h>
@class Car;
@protocol ViewCarProtocol <NSObject>
- (Car*)carToView;
- (void)carViewDone: (BOOL)dataChanged;
@end
將ViewCarProtocol.h導入到ViewCarTableViewController.h中,並添加一行代碼來聲明一個遵守這個協議的委托
@property (weak, nonatomic) id <ViewCarProtocol> delegate;
在ViewCarTableViewController的viewDidLoad:方法中,通過在初始化dataUpdated:方法的代碼的下面添加以下代碼來設置myCar屬性的值:
self.myCar = [self.delegate carToView];
將CarTableViewController設置為委托,方法為向.h文件中導入ViewCarProtocol.h,並剛好在@interface那一行的下面添加以下代碼:
<ViewCarProtocol>
在CarTableViewController.m中,在newCar方法的下面添加協議方法。將carViewDone:保持空白。在prepareForSegue:sender:中不再設置myCar屬性的值。代碼如下,carToView:
- (Car *)carToView {
NSInteger index = [self.tableView indexPathForSelectedRow].row;
return arrayOfCars[index];
}
通過將prepareForSegue:sender:的內容修改為如下代碼,將汽車表格設置為查看汽車的委托:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"ViewSegue"]) {
ViewCarTableViewController *nextController;
nextController = segue.destinationViewController;
// NSInteger index = [self.tableView indexPathForSelectedRow].row;
// nextController.myCar = arrayOfCars[index];
nextController.delegate = self;
}
}
捕捉返回主要汽車表格的切換行為
下圖顯示了當前的轉移行為以及他們如何發生。這裡有三個segue:從汽車列表到查看汽車的push類型,還有從查看汽車場景到make/model編輯場景的兩個model類型。最後還有才能夠make/model編輯場景回退到查看汽車場景,方法是使用dismissControllerAnimated:completion:
從查看汽車場景到汽車表格場景的轉換時個隱式的unwind segue,源自一個並未控制的用戶界面元素。用戶單機導航欄中的Back按鈕,從而segue展開,但並未調用故事版和segue。
然而,存在另一個機制,即Back按鈕由導航控制器進行管理。每當視圖控制器被推入(pushed)或從堆棧中彈出(popped off)時,導航控制器就會查找可選的委托,並能在這次轉場的前後都發送消息
為了捕捉從汽車查看場景返回到汽車表格場景的轉換,需要將正確的控制器設置為導航控制器的委托,然後實現合適的方法。當前協議位於查看汽車表視圖的中間,因此這就是需要監聽場景切換的地方:
將查看汽車表視圖控制器設置為導航控制器的委托,方法是在ViewCarTableViewController.h文件中修改如下代碼:<MakeModelEditProtocol, UINavigationControllerDelegate>
將查看汽車場景的列表控制器設置為導航控制器的委托。在ViewCarTableViewController.m中添加viewWillAppear:方法,放在viewDidLoad:方法的下面:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.navigationController.delegate = self;
}
將下列方法添加到ViewCarTableViewController.m的頂部,以向委托對象發送一條carViewDone:協議消息,只要數據被更新並且下一視圖控制器是委托。此外一處該對象的委托身份,方式是設置導航控制器的委托屬性為nil:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (viewController == (UIViewController*)self.delegate) {
if (dataUpdated) {
[self.delegate carViewDone:dataUpdated];
}
navigationController.delegate = nil;
}
}
最後,實現CarTableViewController.m中的carViewDone:方法:
- (void)carViewDone:(BOOL)dataChanged {
if (dataChanged) {
[self.tableView reloadData];
}
}
運行結果如下:
一些更新策略當使用carViewDone:方法中的reloadTable是我,我們告訴表視圖,每個單元格中的內容是無效的,所有的內容都是過期的。這會導致表視圖至少會對所有的(通常會更多)展示給用戶的單元格進行更新。在應用程序中,這非常快,因為數據簡單。但是當數據復雜又多的時候,這可能就變得很慢了
所以,我們應該只告訴表格更新那些發生改變的單元格
reloadRowsAtIndexPath:withRowAnimation:方法更新特定集合中的單元格。要使用這個方法,必須知道被查看單元格的索引路徑——也即是被選中的單元格。所有我們能夠獲得的被選中的單元格的索引路徑,可以通過使用indexPathForSelectedRow方法得到。步驟如下:
在arrayOfCars下方添加一個狀態常量:@implementation CarTableViewController{
NSMutableArray *arrayOfCars;
NSIndexPath *currentViewCarPath;//添加的
}
通過改變carToView:以設置值
- (Car *)carToView {
// NSInteger index = [self.tableView indexPathForSelectedRow].row;
currentViewCarPath = [self.tableView indexPathForSelectedRow];
// return arrayOfCars[index];
return arrayOfCars[currentViewCarPath.row];
}
最後,更改carViewDone,只更新那些被查看且變化了的單元格
- (void)carViewDone:(BOOL)dataChanged {
// if (dataChanged) {
// [self.tableView reloadData];
// }
if (dataChanged) {
[self.tableView reloadRowsAtIndexPaths:@[currentViewCarPath] withRowAnimation:YES];
}
currentViewCarPath = nil;
}
編輯年份
型號和品牌都基於文本。可以使用文本標示年份並且展示數字文本的鍵盤。但這並不能確保用戶的輸入在特定范圍年數內的4位年份數。相反,可以創建一個基於選擇器的編輯視圖,特定於羅馬時間
UIPickerView控件可以讓我們展示更長的、由單個選項甚至多個分欄組成的列表。鬧鈴就是一個例子
選擇器看似是老虎機,由回轉軸和回轉軸上的多個位置組成。可指定回轉軸上的數字和回轉軸上每個數字的位置。可以通過實現UIPickerViewDataSource協議裡的方法來設置回轉軸
也需要設置每個位置的內容、這一點如同字符串一樣簡單,也可同特定寬度和高度的自定義視圖一樣復雜。可以在UIPickerDelegateProtocol協議的方法中進行設置。委托也會在行被選中時通知我們,這點對於動態的更新選項或者其他部分的用戶界面都是有用的
設置年份編輯器不向老虎機,我們只需要回轉軸。需要將選擇器視圖放置到視圖控制器中作為年份編輯器,步驟如下:
拖入一個視圖控制器到故事板中 將導航欄添加到頂部,並且添加Cancel和Done導航欄按鈕放置一個選擇器視圖,將其約束設置為前後距離0,底部標准距離
添加繼承自UIViewController的類YearEditViewController,並使其成為該類的新場景
為Cancel和Done分別添加action,名稱分別為editCancel和editDone
為選擇器視圖添加一個屬性命名為editPicker所以這時候YearEditViewController.h應該有如下代碼:
- (IBAction)editCancel:(id)sender;
- (IBAction)editDone:(id)sender;
@property (weak, nonatomic) IBOutlet UIPickerView *editPicker;
實現品牌年份選擇器
一輛汽車的品牌年份是個有范圍的數字:不會大於當前年份,也不會小於第一輛生產的汽車
在修改年份編輯器之前,更新Car對象,使用新的最早的品牌年份:
在Car.h文件中,添加代碼#define kModelTYear 1908
修改init方法,以使用kModelTYear代替1900
將Car.h文件導入到YearEditViewController.m文件中
選擇器數學
步驟如下:
通過在YearEditViewController.h文件中的@interface的下方,添加下列代碼以遵從選擇器的兩個方法<UIPickerViewDelegate,UIPickerViewDataSource>
添加代碼,用於獲得年份
```
- (NSInteger)getYearFromDate:(NSData*)theDate {
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];//1 年份要求將日期分解為組件,這需要有日歷。該方法只針對羅馬日了,雖然可以使用當前系統日歷來對返回的值進行本地化
NSDateComponents *components;
components = [gregorian components:NSYearCalendarUnit fromDate:theDate];//2 從theDate返回日期的一個組件對象,用於初始化年份
return components.year; //3 返回日期組件
}
在viewDidLoad的下方插入可設置組和行的數量的日期源方法,在這裡,最大年份是當前年份
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
NSInteger maxYear = [self getYearFromDate:[NSDate date]];
maxYear += 1;
return (maxYear - kModelTYear);
}
返回每行所展示的值,方法是實現字符串的基於標題的委托方法。將該方法置於剛創建的兩個方法的下面:
- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component {
NSInteger totalRows = [pickerView numberOfRowsInComponent:component];
NSInteger displayVal = ((kModelTYear + totalRows) - 1) -row;
return [NSString stringWithFormat:@"%d", displayVal];
}
使用故事板將dataSource和delegate的旋轉指針的outlet於編輯控制器的年份連接起來 在開始測試之前,需要創建一個segue以打開年份編輯器:
將一個展開的附件指示器(disclose Accessory indicator)添加到年份單元格 從年份單元格將一個模態選擇segue拖入年份編輯器,命名該segue為YearEditSegue運行如圖:
協議非常簡單,年份編輯器需要年份用展示,委托需要知道在按鈕Done被選中時所選擇的年份
添加委托需要兩個關鍵的公式。第一個公式設置初始選擇器的行,用於當前的年份。最大的年份選中最頂上的那條,索引為0。最初的想法可能是選中目標年份,然後減去最小的年份數以獲得那行。然而,這並不適用於:年份越大,差異越大,也就是索引數。我們要讓最大年份數減去目標年份:Row=MaximumYear-TargetYear
第二個公式將選中的行轉為年份。再次,這是個相反的問題。索引為0的那行是最大年份:索引越大,年份越小。在此,年份是最大年份減去當前的行數:SelectedYear=MaximumYear-Row
這僅僅得到的是獲得的最大年份,並且是單純的總行數加上最小年份數後減一:MaximumYear=(MinimumYear+TotalRows)-1
最後一種選擇是如何為選擇器展示默認的選中的年份,如果沒有設置值,即汽車對象將年份設置為1908。既然大多數人駕駛的汽車為當前年份前後左右的那些年份,最後的選擇是展示這一點。可以展示默認年份為1908,但是駕駛1908年汽車的人概率太小了
遵循下列步驟以創建協議和相關的屬性與方法:
添加新的協議,命名為YearEditProtocol,並添加下列方法聲明:#import <Foundation/Foundation.h>
@protocol YearEditProtocol <NSObject>
- (NSInteger) editYearValue;
- (void) editYearDone:(NSInteger)yearValue;
@end
導入協議到YearEditViewController.h文件中並添加一個屬性以遵從委托,使用下列代碼:
@property (weak, nonatomic) id <YearEditProtocol> delegate;
在.m文件中,從方法中初始化年份選擇器。可以通過使用selectRow:inComponent:animated方法設置選擇器的選中內容:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view
NSInteger yearValue = [self.delegate editYearValue];
if (yearValue == kModelTYear) {
yearValue = [self getYearFromDate:[NSDate date]];
}
NSInteger rows = [self.editPicker numberOfRowsInComponent:0];
NSInteger maxYear = (kModelTYear + rows) - 1;
NSInteger row = maxYear -yearValue;
[self.editPicker selectRow:row inComponent:0 animated:YES];
}
填寫好editCancel:和editDone
- (IBAction)editCancel:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)editDone:(id)sender {
NSInteger rows = [self.editPicker numberOfRowsInComponent:0];
NSInteger maxYear = (kModelTYear + rows) -1;
NSInteger year = maxYear - [self.editPicker selectedRowInComponent:0];
[self.delegate editYearDone:year];
[self dismissViewControllerAnimated:YES completion:nil];
}
@end
導入年份編輯協議到ViewCarTableViewController.h文件中並添加該協議到已有的協議中
<MakeModelEditProtocol, UINavigationControllerDelegate,YearEditProtocol>
打開ViewCarTableViewController.m文件並導入YearEditViewController.h
將最後的else if條件語句添加到 prepareForSegue:sender:方法中
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"MakeEditSegue"]) {
MakeModelEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
currentEdutType = kCurrentEditMake;
} else if ([segue.identifier isEqualToString:@"ModelEditSegue"]) {
MakeModelEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
currentEdutType = kCurrentEditModel;
} else if ([segue.identifier isEqualToString:@"YearEditSegue"]) {//下面是新加的
YearEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
}
}
將協議方法添加到ViewCarTableViewController.m文件中,當年份改變時,記住更新汽車的值、標簽和用於更新狀態變量的數據
- (NSInteger)editYearValue {
return self.myCar.year;
}
- (void)editYearDone:(NSInteger)yearValue {
if(yearValue != self.myCar.year) {
self.myCar.year = yearValue;
self.yearLabel.text = [NSString stringWithFormat:@"%d",self.myCar.year];
dataUpdated = YES;
}
最後的運行結果如圖:
今天就介紹到這裡咯
可以到ArnoldYU ,我的git上下載代碼
我的另一個博客站點:Arnold-你們好啊
【IOS開發入門(12)】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!