很多應用都會在界面中使用某種列表控制項:用戶可以選中、刪除或重新排列列表中的項目。這些控制項其實都是UITableView 對象,可以用來顯示一組對象,例如,用戶地址薄中的一組人名。 UITableView 對象雖然只能顯示一行數據,但是沒有行數限制。 編寫新的應用程式 JXHomepwner 應用 創 ...
很多應用都會在界面中使用某種列表控制項:用戶可以選中、刪除或重新排列列表中的項目。這些控制項其實都是UITableView 對象,可以用來顯示一組對象,例如,用戶地址薄中的一組人名。
UITableView 對象雖然只能顯示一行數據,但是沒有行數限制。
- 編寫新的應用程式 JXHomepwner 應用
創建應用,填寫基本信息
- UITableViewController
UITableView 是視圖。我們知道 模型-視圖-控制器(Model-View-Controller),他是我們必須遵守的一種設計模式。其含義是,應用創建的任何一個對象,其類型必定是以下三種類型中的一種。
1. 模型:負責存儲數據,與用戶界面無關。
2. 視圖:負責顯示界面,與模型對象無關。
3. 控制器:負責確保視圖對象和模型對象的數據保持一致。
一般來說,作為視圖對象的 UITableView 不應該負責處理應用的邏輯或數據。當在應用中使用 UITableView 對象的時候,必須考慮如何大啊呸其他的對象,與 UITableView 對象一起工作:
通常情況下,要通過某個視圖控制器對象來創建和釋放 UITableView 對象,並負責顯示或者隱藏視圖。
UITableView 對象要有數據源才能正常工作。UITableView 對象會向數據源查詢要顯示的行數,顯示表格行所需要的數據和其他所需要的數據。沒有數據源的 UITableView 對象只是空殼。凡是遵守 UITableViewDataSource 協議的對象,都可以成為 UITableView 對象的數據源(即dataSource屬性所指向的對象)。
通常情況下,要為 UITableView 對象設置委托對象,以便能在該對象發生特定事件的時候做出相應的處理。凡是遵守 UITableViewDelegate 協議的對象,都可以成為 UITableView 對象的委托對象。
UITableViewController 對象可以扮演以上全部角色,包括視圖控制器對象、數據源和委托對象。
UITableViewController 是 UIViewController 的子類,所以也有 view 屬性。UITableViewController 對象的 view 屬性指向一個 UITableView 對象,並且這個對象由 UITableViewController 對象負責設置和顯示。UITableViewController 對象會在創建 UITableView 對象後,為這個 UITableView 對象的 dataSource 和 delegate 賦值,並指向自己。
- 創建 UITableViewController 子類
下麵要為我們創建的程式編寫一個 UITableViewController 子類。
UITableViewController 的指定初始化方法是 initWithStyle: 調用 initWithStyle: 時要傳入一個類型作為 UITableViewStyle 的常熟,該常熟決定了 UITableView 對象的風格。目前可以使用的 UITableViewStyle 常量有兩個,即 UITableViewStylePlain 和 UITableViewStyleGrouped 。
現在將 UITableViewController 的指定初始化方法改為 init: ,為此時需要遵守兩條規則:
1. 在新的指定初始化方法中調用父類的指定初始化方法。
2. 覆蓋父類的初始化方法,調用新的指定初始化方法。
#import "JXItemsViewController.h" @interface JXItemsViewController () @end @implementation JXItemsViewController - (instancetype)init { // 調用父類的指定初始化方法 self = [super initWithStyle:UITableViewStylePlain]; return self; } - (instancetype)initWithStyle:(UITableViewStyle)style { return [self init]; } @end
實現以上兩個初始化方法之後,可以確保無論向新創建的 JXItemsViewController 對象發送哪一個初始化方法,初始化後的對象都會使用我們指定的風格。
接下來代碼如下:
#import "AppDelegate.h" #import "JXItemsViewController.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; // 添加初始化代碼 // 創建 JXItemsViewController 對象 JXItemsViewController * itemsViewController = [[JXItemsViewController alloc] init]; // 將 JXItemsViewController 的標示圖加入視窗 self.window.rootViewController = itemsViewController; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; }
構建並運行應用我們確實能在屏幕上看到 UITableView 對象。JXItemsViewController 作為 UITableViewController 的子類,集成了 view 方法。 view 方法會調用 loadView 方法,如果視圖不存在,則 loadView 方法會創建並載入一個空的視圖。
下麵我們要為 UITableView 設置內容。
新建 JXItem 類
#import <Foundation/Foundation.h> @interface JXItem : NSObject /** 創建日期 */ @property (nonatomic,strong,readonly) NSDate * createDate; /** 名稱 */ @property (nonatomic,strong) NSString * itemName; /** 編號 */ @property (nonatomic,strong) NSString * serialnumber; /** 價值 */ @property (nonatomic,assign) NSInteger valueInDollars; /** JXImageStore中的鍵 */ @property (nonatomic,strong) NSString * itemKey; + (instancetype)randomItem; /** * JXItem類指定的初始化方法 * @return 類對象 */ - (instancetype)initWithItemName:(NSString *)name valueInDollars:(NSInteger)value serialNumber:(NSString *)sNumber; - (instancetype)initWithItemName:(NSString *)name; @end
#import "JXItem.h" @implementation JXItem + (instancetype)randomItem { // 創建不可變數組對象,包含三個形容詞 NSArray * randomAdjectiveList = @[ @"Fluffy", @"Rusty", @"Shiny" ]; // 創建不可變數組對象,包含三個名詞 NSArray * randomNounList = @[ @"Bear", @"Spork", @"Mac" ]; // 根據數組對象所含的對象的個數,得到隨機索引 // 註意:運算符%是模運算符,運算後得到的是餘數 NSInteger adjectiveIndex = arc4random() % randomAdjectiveList.count; NSInteger nounIndex = arc4random() % randomNounList.count; // 註意,類型為NSInteger 的變數不是對象 NSString * randomName = [NSString stringWithFormat:@"%@ %@",randomAdjectiveList[adjectiveIndex],randomNounList[nounIndex]]; NSInteger randomValue = arc4random_uniform(100); NSString * randomSerialNumber = [NSString stringWithFormat:@"%c%c%c%c", '0' + arc4random_uniform(10), 'A' + arc4random_uniform(26), '0' + arc4random_uniform(10), 'A' + arc4random_uniform(26)]; JXItem * newItem = [[self alloc] initWithItemName:randomName valueInDollars:randomValue serialNumber:randomSerialNumber]; return newItem; } - (NSString *)description { NSString * descriptionString = [NSString stringWithFormat:@"%@ (%@):Worth $%zd, recorded on %@",self.itemName,self.serialnumber,self.valueInDollars,self.createDate]; return descriptionString; } - (instancetype)initWithItemName:(NSString *)name valueInDollars:(NSInteger)value serialNumber:(NSString *)sNumber { // 調用父類的指定初始化方法 self = [super init]; // 父類的指定初始化方法是否成功創建了對象 if (self) { // 為實例變數設置初始值 _itemName = name; _valueInDollars = value; _serialnumber = sNumber; // 設置_createDate為當前時間 _createDate = [NSDate date]; // 創建一個 NSUUID 對象 NSUUID * uuid = [[NSUUID alloc] init]; NSString * key = [uuid UUIDString]; _itemKey = key; } // 返回初始化後的對象的新地址 return self; } - (instancetype)initWithItemName:(NSString *)name { return [self initWithItemName:name valueInDollars:0 serialNumber:@""]; } - (instancetype)init { return [self initWithItemName:@"Item"]; } - (void)dealloc { NSLog(@"Destoryed:%@",self); } @end
- UITableView 數據源
創建JXItemStore
JXItemStore 對象是一個單例,也就是說,每個應用只會有一個這種類型的對象。如果應用嘗試創建另一個對象,JXItemStore類就會返回已經存在的那個對象。當某個程式要在很多不同的代碼段中使用同一個對象時,將這個對象設置為單例是一種很好的設計模式,只需要向該對象的類發送特定的方法,就可以得到相同的對象。
#import <Foundation/Foundation.h> @interface JXItemStore : NSObject // 註意,這是一個類方法,首碼是+ + (instancetype)sharedStore; @end
在 JXItemStore 類收到 sharedStore 消息後,會檢查自己是否已經創建 JXItemStore 的單例對象。如果已經創建,就返回自己已經創建的對象,否則就需要先創建,然後再返回。
#import "JXItemStore.h" @implementation JXItemStore // 單粒對象 + (instancetype)sharedStore { static JXItemStore * sharedStore = nil; // 判斷是否需要創建一個 sharedStore 對象 if (!sharedStore) { sharedStore = [[self alloc] init]; } return sharedStore; } @end
這段代碼將 sharedStore 指針聲明瞭 靜態變數。當某個定義了靜態變數的方法返回時,程式不會釋放相應的變數。靜態變數和全局變數一樣,並不是保存在棧中的。
sharedStore 變數的初始值為 nil。當程式第一次執行 sharedStore 方法時,會創建一個 JXItemStore 對象,並將新創建的對象的地址賦值給 sharedStore 變數。當程式再次執行 sharedStore 方法時,不管是第幾次,其指針總是指向最初創建的那個對象。因為指向 JXItemStore 對象的 sharedStore 變數是強引用,且程式永遠不會釋放該變數,所以 sharedStore 變數所指向的 JXItemStore 對象永遠也不會被釋放。
JXItemsViewController 需要創建一個新的 JXItem 對象時會向 JXItemStore 對象發送消息,收到消息的 JXItemStore 對象會創建一個 JXItem 對象並將其保存到一個 JXItem 數組中,之後 JXItemsViewController 可以通過該數組獲取所有 JXItem 對象,並使用這些對象填充自己的表視圖。
#import <Foundation/Foundation.h> @class JXItem; @interface JXItemStore : NSObject /** 存放 JXItem 對象數組 */ @property (nonatomic,readonly) NSArray * allItem; // 註意,這是一個類方法,首碼是+ + (instancetype)sharedStore; - (JXItem *)createItem; @end
在實現文件中編輯。但是我們需要註意,在我們的應用中將使用 JXItemStore 管理 JXItem 數組-包括添加、刪除和排序。因此,除 JXItemStore 之外的類不應該對 JXItem 數組做這些操作。在 JXItemStore 內部,需要將 JXItem 數組定義為可變數組。而對其他類來說,JXItem 數組則是不可變的數組。這是一種常見的設計模式,用於設置內部數據的訪問許可權:某個對象中有一種可修改的數據,但是除該對象本身之外,其他對象只能訪問該數據而不能修改它。
#import "JXItemStore.h" #import "JXItem.h" @interface JXItemStore () /** 可變數組,用來操作 JXItem 對象 */ @property (nonatomic,strong) NSMutableArray * privateItems; @end @implementation JXItemStore // 單粒對象 + (instancetype)sharedStore { static JXItemStore * sharedStore = nil; // 判斷是否需要創建一個 sharedStore 對象 if (!sharedStore) { sharedStore = [[self alloc] init]; } return sharedStore; } - (NSArray *)allItem { return self.privateItems; } #pragma mark - 懶載入 - (NSMutableArray *)privateItems{ if (_privateItems == nil) { _privateItems = [[NSMutableArray alloc] init]; } return _privateItems; } @end
allItem 方法的返回值是 NSArray 類型,但是方法體中返回的是 NSMutableArray 類型的對象,這種寫法是正確的,因為NSMutableArray 是 NSArray 子類。
這種寫法可能會引起一個問題:雖然頭文件中將 allItem 的類型聲明為 NSArray ,但是其他對象調用 JXItemStore 的 allItem 方法時,得到的一定是一個 NSMutableArray 對象。
使用像 JXItemStore 這樣的類時,應該遵守其頭文件中的聲明使用類的屬性和方法。 例如,在 JXItemStore 頭文件中,因為 allItem 屬性的類型是 NSArray ,所以應該將其作為 NASrray 類型的對象使用。如果將 allItem 轉換為 NSMutableArray 類型並修改其內容,就違反了 JXItemStore 頭文件中的聲明。可以通過覆蓋 allItem 方法避免其他類修改 allItem 。
#import "JXItemStore.h" #import "JXItem.h" @interface JXItemStore () /** 可變數組,用來操作 JXItem 對象 */ @property (nonatomic,strong) NSMutableArray * privateItems; @end @implementation JXItemStore // 單粒對象 + (instancetype)sharedStore { static JXItemStore * sharedStore = nil; // 判斷是否需要創建一個 sharedStore 對象 if (!sharedStore) { sharedStore = [[self alloc] init]; } return sharedStore; } - (NSArray *)allItem { return [self.privateItems copy]; } - (JXItem *)createItem { JXItem * item = [JXItem randomItem]; [self.privateItems addObject:item]; return item; } #pragma mark - 懶載入 - (NSMutableArray *)privateItems{ if (_privateItems == nil) { _privateItems = [[NSMutableArray alloc] init]; } return _privateItems; } @end
實現數據源方法
#import "JXItemsViewController.h" #import "JXItem.h" #import "JXItemStore.h" @interface JXItemsViewController () @end @implementation JXItemsViewController - (instancetype)init { // 調用父類的指定初始化方法 self = [super initWithStyle:UITableViewStylePlain]; if (self) { for (NSInteger i=0; i<5; i++) { [[JXItemStore sharedStore] createItem]; } } return self; } - (instancetype)initWithStyle:(UITableViewStyle)style { return [self init]; } @end
當某個 UITableView 對象要顯示表格內容時,會向自己的數據源(dataSource 屬性所指向的對象)發送一系列消息,其中包括必須方法和可選方法。
@required - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; // Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier: // Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls) - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; @optional - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView; // Default is 1 if not implemented - (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section; // fixed font style. use custom view (UILabel) if you want something different - (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section; // Editing // Individual rows can opt out of having the -editing property set for them. If not implemented, all rows are assumed to be editable. - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath; // Moving/reordering // Allows the reorder accessory view to optionally be shown for a particular row. By default, the reorder control will be shown only if the datasource implements -tableView:moveRowAtIndexPath:toIndexPath: - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath; // Index - (nullable NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView __TVOS_PROHIBITED; // return list of section titles to display in section index view (e.g. "ABCD...Z#") - (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index __TVOS_PROHIBITED; // tell table which section corresponds to section title/index (e.g. "B",1)) // Data manipulation - insert and delete support // After a row has the minus or plus button invoked (based on the UITableViewCellEditingStyle for the cell), the dataSource must commit the change // Not called for edit actions using UITableViewRowAction - the action's handler will be invoked instead - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath; // Data manipulation - reorder / moving support - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; @end
實現行數代碼
#import "JXItemsViewController.h" #import "JXItem.h" #import "JXItemStore.h" @interface JXItemsViewController () @end @implementation JXItemsViewController - (instancetype)init { // 調用父類的指定初始化方法 self = [super initWithStyle:UITableViewStylePlain]; if (self) { for (NSInteger i=0; i<5; i++) { [[JXItemStore sharedStore] createItem]; } } return self; } - (instancetype)initWithStyle:(UITableViewStyle)style { return [self init]; } - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - Table view data source - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[JXItemStore sharedStore] allItem] count]; } @end
UITableViewDataSource 協議中的另外一個必須實現的方法
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
在此之前,我們需要先瞭解另一個類:UITableViewCell
- UITableViewCell 對象
表視圖所顯示的每一行都是一個獨立的視圖,這些視圖是 UITableViewCell 對象。其對象還有一個子視圖:contentView 。contentView 也包含了很多子視圖,他的子視圖構成 UITableViewCell 對象的主要外觀。此外, UITableViewCell 對象還可以顯示一個輔助指示圖。輔助指示視圖的作用是顯示一個指定的圖標,用於向用戶提示 UITableViewCell 對象可以執行的動作。這些圖標包括勾起標記、展開圖標或中間有v形團的藍色圓點。其預設是 UITableViewCellAccessoryNone 。
在創建 UITableViewCell 對象時,可以選擇不同的風格來決定 UITableViewCell 對象顯示。
typedef NS_ENUM(NSInteger, UITableViewCellStyle) { UITableViewCellStyleDefault, // Simple cell with text label and optional image view (behavior of UITableViewCell in iPhoneOS 2.x) UITableViewCellStyleValue1, // Left aligned label on left and right aligned label on right with blue text (Used in Settings) UITableViewCellStyleValue2, // Right aligned label on left with blue text and left aligned label on right (Used in Phone/Contacts) UITableViewCellStyleSubtitle // Left aligned label on top and left aligned label on bottom with gray text (Used in iPod). }; // available in iPhone OS 3.0
創建並獲取 UITableViewCell 對象
下麵我們主要對 tableView: cellForRowAtIndexPath: 方法進行改寫。首先我們需要將 JXItem 數據跟 UITableViewCell 對象對應起來。在方法中有一個實參是 NSIndexPath 對象,該對象包含兩個屬性 section(段) 和 row(行) 。當 UITableView 對象向其數據源發送 tableView: cellForRowAtIndexPath: 消息時,其目的是獲取顯示第 section 個表格段、第 row 行數據的 UITableViewCell 對象。
#import "JXItemsViewController.h" #import "JXItem.h" #import "JXItemStore.h" @interface JXItemsViewController () @end @implementation JXItemsViewController - (instancetype)init { // 調用父類的指定初始化方法 self = [super initWithStyle:UITableViewStylePlain]; if (self) { for (NSInteger i=0; i<15; i++) { [[JXItemStore sharedStore] createItem]; } } return self; } - (instancetype)initWithStyle:(UITableViewStyle)style { return [self init]; } - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - Table view data source - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[JXItemStore sharedStore] allItem] count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { // 創建 UITableViewCell 對象,風格使用預設風格 UITableViewCell * cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"]; // 獲取 allItem 的第 n 個 JXItem 對象 // 然後將該 JXItem 對象的描述信息賦值給 UITableViewCell 對象的 textLabel // 這裡的 n 是該 UITableViewCell 對象所對應的表格索引 NSArray * items = [[JXItemStore sharedStore] allItem]; JXItem * item = items[indexPath.row]; cell.textLabel.text = [item description]; return cell; } @end
構建並運行
重用UITableViewCell對象
iOS設備記憶體是有限的,如果某個 UITableView 對象要顯示大量的記錄,並且要針對每條記錄創建相應的 UITableViewCell 對象,就會很快耗盡iOS設備記憶體。
在 UITableView 上存在大量可優化的地方,其中最重要的就是關於 UITableViewCell 復用問題。因為當我們滑動界面是,大多數的 cell表格都會移出視窗,移出視窗的 UITableViewCell 對象放入 UITableViewCell 對象池,等待重用。當 UITableView 對象要求數據源返回某個 UITableViewCell 對象時,就可以先查看對象池。如果有未使用的 UITableViewCell 對象,就可以用新的數據配置這個 UITableViewCell 對象,然後將其返回給 UITableView 對象,從而避免了創建新的對象,可以極大的優化記憶體。
但是這裡還會有一個問題:如果我們在 UITableView 對象中創建了不同的 UITableViewCell 表格,用來展示不同的信息。那麼這時候 UITableViewCell 對象池中的對象就會存在不同的類型,那麼 UItableView 就有可能會得到錯誤的類型的 UITableViewCell 對象。鑒於上述原因,必須保證 UITableView 對象能夠得到正確的指定類型的 UITableViewCell 對象,這樣才能確定返回的對象會擁有哪些屬性和方法。
從 UITableViewCell 對象池獲取對象時,無需關心取回的是否是某個特性的對象,因為無論取回來的是哪個對象,都要重新設置數據。真正要關心的是取回來的對象是否是某個特性的類型。每個 UITableViewCell 對象都有一個類型為 NSString 的 reuseIdentifier 屬性。當數據源向 UITableView 對象獲取可重用的 UITableViewCell 對象時,可傳入一個字元串並要求 UITableView 對象返回相應的 UITableViewCell 對象。
#import "JXItemsViewController.h" #import "JXItem.h" #import "JXItemStore.h" @interface JXItemsViewController () @end @implementation JXItemsViewController - (instancetype)init { // 調用父類的指定初始化方法 self = [super initWithStyle:UITableViewStylePlain]; if (self) { for (NSInteger i=0; i<15; i++) { [[JXItemStore sharedStore] createItem]; } } return self; } - (instancetype)initWithStyle:(UITableViewStyle)style { return [self init]; } - (void)viewDidLoad { [super viewDidLoad]; // 向控制器註冊 [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"]; } #pragma mark - Table view data source - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[JXItemStore sharedStore] allItem] count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { // 創建 UITableViewCell 對象,風格使用預設風格 UITableViewCell * cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"]; UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath]; // 獲取 allItem 的第 n 個 JXItem 對象 // 然後將該 JXItem 對象的描述信息賦值給 UITableViewCell 對象的 textLabel // 這裡的 n 是該 UITableViewCell 對象所對應的表格索引 NSArray * items = [[JXItemStore sharedStore] allItem]; JXItem * item = items[indexPath.row]; cell.textLabel.text = [item description]; return cell; } @end