在Cocoa Touch中,为UITableView对象设置表格行的流程与面向过程的编程模式不同。如果是面向过程的编程模式,就要“告诉”UITableView对象应该显示什么内容。在Cocoa Touch中,UITableView对象会自己查询另一个对象以获得需要显示的内容,这个对象就是UITableView对象的数据源,也就是dataSource属性所指向的对象。以BNRItemsViewController对象的UITableView对象为例,UITableView对象的数据源就是BNRItemsViewController对象自己。所以下面要为BNRItemsViewController对象添加相应的属性和方法,使其能够保存多个BNRItem对象。
第2章中的RandomItems应用使用了一个NSMutableArray对象来保存多个BNRItem对象。Homepwner也要使用相同的方式,但是要稍作修改:将用于保存BNRItem对象的NSMutableArray对象抽象为BNRItemStore对象(见图8-6)。这里为什么使用的是NSMutableArray而不是NSArray?因为BNRItemStore对象同样需要负责保存和加载BNRItem对象。
图8-6 Homepwner对象图
当某个对象需要访问所有的BNRItem时,可以通过BNRItemStore获取包含所有BNRItem的NSMutableArray。之后的章节还会为BNRItemStore添加操作数组的功能,例如添加、删除和排序。此外,BNRItemStore还会负责将BNRItem存入文件,或者从文件重新载入。
创建BNRItemStore
选择File菜单中的New菜单项,然后选择File…创建一个新的NSObject子类并将其命名为BNRItemStore。
BNRItemStore对象是一个单例。也就是说,每个应用只会有一个这种类型的对象。如果应用尝试创建另一个对象,BNRItemStore类就会返回已经存在的那个对象。当某个程序要在很多不同的代码段中使用同一个对象时,将这个对象设置为单例会很方便,只要向该对象的类发送特定的方法,就可以得到相同的对象。
在BNRItemStore.h中声明sharedStore类方法,代码如下:
#import <Foundation/Foundation.h>
@interface BNRItemStore : NSObject
// 注意,这是一个类方法,前缀是+,不是-
+ (instancetype)sharedStore;
@end
在BNRItemStore类收到sharedStore消息后,会检查是否已经创建BNRItemStore的单例对象。如果已经创建,就返回已有的对象,否则先创建再返回。
在BNRItemStore.m中实现sharedStore,同时编写一个抛出异常的init方法和私有指定初始化方法initPrivate。
@implementation BNRItemStore
+ (instancetype)sharedStore
{
static BNRItemStore *sharedStore = nil;
// 判断是否需要创建一个sharedStore对象
if (!sharedStore) {
sharedStore = [[self alloc] initPrivate];
}
return sharedStore;
}
// 如果调用[[BNRItemStore alloc] init],就提示应该使用[BNRItemStore sharedStore]
- (instancetype)init
{
@throw [NSException exceptionWithName:@/"Singleton/"
reason:@/"Use +[BNRItemStore sharedStore]/"
userInfo:nil];
return nil;
}
// 这是真正的(私有的)初始化方法
- (instancetype)initPrivate
{
self = [super init];
return self;
}
这段代码将sharedStore指针声明为了静态变量(static variable)。当某个定义了静态变量的方法返回时,程序不会释放相应的变量。静态变量和全局变量(global variable)一样,并不是保存在栈中的。
sharedStore变量的初始值是nil。当程序第一次执行sharedStore方法时,会创建一个BNRItemStore对象,并将新创建的对象的地址赋给sharedStore变量。当程序再次执行sharedStore方法时,无论是第几次,sharedStore变量仍然会指向最初的那个BNRItemStore对象。因为指向BNRItemStore对象的sharedStore变量是强引用,且程序永远不会释放该变量,所以sharedStore变量所指向的BNRItemStore对象也永远不会被释放。
BNRItemStore需要创建一个新的BNRItem对象时会向BNRItemStore对象发送消息,收到消息的BNRItemStore对象会创建BNRItem对象并将其保存到一个BNRItem数组中,之后BNRItemsViewController可以通过该数组获取所有BNRItem对象,并使用这些对象填充自己的表视图。
在BNRItemStore.h中声明一个方法和一个属性,分别用于创建和保存BNRItem对象。
#import <Foundation/Foundation.h>
@class BNRItem;
@interface BNRItemStore : NSObject
@property (nonatomic, readonly) NSArray *allItems;
+ (instancetype)sharedStore;
- (BNRItem *)createItem;
@end
这段代码使用了@class指令。该指令的作用是告诉编译器,某处代码定义了一个名为BNRItem的类。当某个文件只需要使用BNRItem类的声明,无须知道具体的实现细节时,就可以使用该指令。使用该指令后,不用在BNRItemStore.h中导入BNRItem.h,就能将createItem方法的返回类型声明为指向BNRItem对象的指针。当某个类的头文件发生变化时,对那些通过@class指令声明该类的其他文件,编译器可以不用重新编译,这样就可以大幅节省编译时间。
在另一些文件中,程序会向BNRItem类或BNRItem对象发送消息。对这些文件,就必须导入BNRItem的头文件,使编译器知道所有的实现细节。在BNRItemStore.m顶部导入BNRItem.h,以便之后向BNRItem对象发送消息,代码如下:
#import /"BNRItemStore.h/"
#import /"BNRItem.h/"
请注意,Homepwner将使用BNRItemStore管理BNRItem数组——包括添加、删除和排序。因此,除BNRItemStore之外的类不应该对BNRItem数组做这些操作。在BNRItemStore内部,需要将BNRItem数组定义为可变数组。而对于其他类来说,BNRItem数组则是不可变数组。这是一种常见的设计模式,用于设置内部数据的访问权限:某个对象中有一种可修改的数据,但是除该对象之外,其他对象只能访问该数据而不能修改它。例如,在之前的代码中, allItems属性被声明为NSArray类型(不可变数组),并将其设置为readonly。这样,其他类既无法将一个新数组赋给allItems,也无法修改allItems。
接下来在BNRItemStore.m的类扩展中声明一个可变数组。
#import /"BNRItem.h/"
@interface BNRItemStore
@property (nonatomic) NSMutableArray *privateItems;
@end
@implementation BNRItemStore
然后实现initPrivate方法,初始化privateItems属性。同时还需要覆盖allItems的取方法,返回privateItems。
- (instancetype)initPrivate
{
self = [super init];
if (self) {
_privateItems = [[NSMutableArray alloc] init];
}
return self;
}
- (NSArray *)allItems
{
return self.privateItems;
}
allItems方法的返回值是NSArray类型,但是方法体中返回的是NSMutableArray类型的对象,这种写法是正确的,因为NSMutableArray是NSArray的子类。读者可以将NSMutableArray看成是一种特殊的NSArray,它具有NSArray的所有功能。(请注意,如果allItems的类型是NSMutableArray,而privateItems的类型是NSArray,那么这种写法就是错误的,因为NSArray没有NSMutableArray中关于修改数组的功能。)
这种写法可能会引起一个问题:虽然头文件中将allItems的类型声明为NSArray,但是其他对象调用BNRItemStore的allItems方法时,得到的一定是一个NSMutableArray对象——Objective-C对象知道自己的类型,无论是属性声明还是返回值类型声明都不会修改对象类型。
使用像BNRItemStore这样的类时,应该遵循其头文件中的声明使用类的属性和方法,例如,在BNRItemStore头文件中,因为allItems属性的类型是NSArray,所以应该将其作为NSArray类型的对象使用。如果将allItems转换为NSMutableArray类型并修改其内容,就违反了BNRItemStore头文件中的声明。可以通过覆盖allItems方法避免其他类修改allItems:在allItems方法中使用copy方法返回privateItems属性的不可变副本(对应于copy方法,还有一个mutableCopy方法可以返回相应的可变副本),类似于以下代码:
- (NSArray *)allItems
{
return [self.privateItems copy];
}
以上代码没有使用黑体标注的原因是,建议读者不要编写这类代码。其实只要遵循编程约定(这里的约定是:遵循头文件中的声明),就不会出现这类问题。
在BNRItemStore.m中,按照之前介绍的方式实现createItem方法:
- (BNRItem *)createItem
{
BNRItem *item = [BNRItem randomItem];
[self.privateItems addObject:item];
return item;
}
现在请读者回顾第3章中有关属性合成的知识。BNRItemStore.h将allItems声明为只读属性,而BNRItemStore.m又覆盖了allItems的取方法,因此编译器不会为allItems生成取方法和实例变量_allItems。
实现数据源方法
在BNRItemsViewController.m顶部导入BNRItemStore.h和BNRItem.h。然后更新指定初始化方法,创建5个随机的BNRItem对象并加入BNRItemStore对象,代码如下:
#import /"BNRItemsViewController.h/"
#import /"BNRItemStore.h/"
#import /"BNRItem.h/"
@implementation BNRItemsViewController
- (instancetype)init
{
// 调用父类的指定初始化方法
self = [super initWithStyle:UITableViewStylePlain];
if (self) {
for (int i = 0; i < 5; i++) {
[[BNRItemStore sharedStore] createItem];
}
}
return self;
}
将若干BNRItem对象加入BNRItemStore对象后,下面要让BNRItemsViewController对象将这些BNRItemStore对象转变成UITableView对象可以显示的表格行。当某个UITableView对象要获取显示的数据时,会向其数据源发送一组特定的消息。这些消息都是在UITableViewDataSource协议中声明的。
选择Help菜单中的Documentation and API Reference,搜索UITableViewDataSource协议的参考文档,然后选中左侧面板中的Tasks(见图8-7)。
图8-7 UITableViewDataSource协议的参考文档
第一个task是Configuring a Table View(配置表视图),其中包含了一系列相关方法,请注意这些方法中有两个被标记为了required method(必需方法)。要让BNRItemsViewController遵守UITableViewDataSource协议,就必须为BNRItemsViewController实现tableView:numberOfRowsInSection:和tableView:cellForRowAtIndexPath:这两个必需方法。UITableView对象可以通过数据源对象的这两个方法获得应该显示的行数及显示各行所需的视图。
当某个UITableView对象要显示表格内容时,会向自己的数据源(dataSource属性所指向的对象)发送一系列消息,其中包括必需方法和可选方法。tableView: numberOfRowsInSection:(必需方法)会返回一个整型值,代表UITableView对象显示的行数。对于Homepwner中的UITableView对象来说,BNRItemStore中的每个BNRItem对象都应该对应一个表格行。
在BNRItemsViewController.m中实现tableView:numberOfRowsInSection:方法:
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return [[[BNRItemStore sharedStore] allItems] count];
}
请注意该方法的返回值是一个NSInteger类型的整数。从Apple开始支持64位应用之后,整型值在32位应用中应该是一个32位的整数,而在64位应用中应该是一个64位的整数。因此Apple使NSInteger(有符号整型)和NSUInteger(无符号整型)在32位和64位应用中表示不同位数的整数。为了使应用适配32位和64位设备,请读者使用以上类型代替int。
传入tableView:numberOfRowsInSection:方法的section参数起什么作用?UITableView对象可以分段显示数据,每个表格段(section)包含一组独立的行。以通讯录(Contacts)应用为例,所有以字母“D”开头的名字都会被归在一个表格段中。UITableView对象默认只有一个表格段。本章中的Homepwner应用也只会使用一个表格段。一旦读者理解了UITableView对象的工作原理,就能很容易地实现多个表格段。这也是本章结尾处的第一个练习。
UITableViewDataSource协议中的另外一个必须实现的方法是tableView: cellForRowAtIndexPath:。在实现该方法前,需要先介绍另一个类:UITableViewCell。