很多应用都会在界面中使用某种列表控件:用户可以选中、删除或重新排列列表中的项目。这些控件其实都是UITableView 对象,可以用来显示一组对象,例如,用户地址薄中的一组人名。
UITableView 对象虽然只能显示一行数据,但是没有行数限制。
创建应用,填写基本信息
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 对象的 class="cnblogs_code">dataSource 和 delegate 赋值,并指向自己。
下面要为我们创建的程序编写一个 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
创建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 对象。其对象还有一个子视图: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