首页 » iOS编程(第4版) » iOS编程(第4版)全文在线阅读

《iOS编程(第4版)》23.2 用Core Data重写BNRItemStore的数据保存功能

关灯直达底部

目前,Homepwner是通过固化存取数据的。当模型对象的数量较少时(比如少于1000个),使用固化不会有问题。但是随着模型对象的增多,固化的整存整取特性会产生效率问题,所以需要改用一种能够增量存取、增量更新数据的机制。Core Data可以胜任这项任务。

模型文件

关系数据库有一个名为表格(table)的概念。一张表格代表一种类型的事物,可以是人、信用卡购物记录或房地产列表。每张表格可以有很多列,用于保存相应事物的特定信息。例如,代表人的表格可以有不同的列,分别代表姓名、生日和身高。此外,表中的一行代表一个人。

关系数据库的这种组织关系可以很好地“翻译”至Objective-C。表格对应Objective-C类;表格的列对应类的属性;表格的行对应类的对象。Core Data的作用就是在关系模型和对象模型之间来回移动数据(见图23-1)。

图23-1 Core Data的作用

针对上述概念,Core Data使用的是另一套术语。在Core Data中,表格/类称为实体(entity),列/属性称为实体属性(attribute)。使用Core Data的模型文件可以描述特定的实体和相应的实体属性。下面用模型文件来描述BNRItem实体,以及相应的实体属性,例如itemName、serialNumber和valueInDollars。

打开Homepwner.xcodeproj。选择File菜单中的New菜单项,然后选择New File…选中窗口左侧iOS部分的Core Data,然后选中窗口右侧的Data Model并单击Next按钮,最后将文件命名为Homepwner(后缀名使用默认的),如图23-2所示。

图23-2 创建模型文件

Xcode会将新创建的Homepwner.xcdatamodeld文件加入项目。在项目导航面板中选中该文件,Xcode会在编辑器区域显示相应的用户界面,通过该界面可编辑Core Data模型文件。

单击位于编辑器区域左下角的Add Entity(增加实体)按钮,Xcode会为模型文件增加一个新的实体,并将该实体加入左侧列表的ENTITIES区域。双击新增加的实体,将实体名改为BNRItem(见图23-3)。

图23-3 创建BNRItem实体

下面为BNRItem实体增加实体属性,这些实体属性将成为BNRItem类的属性。单击Attributes区域下方的+按钮可以加入新的实体属性。根据以下列表,增加实体属性并设置相应的Attribute(实体属性名)和Type(实体属性类型)。

•itemName, String(字符串)。

•serialNumber, String(字符串)。

•valueInDollars, Integer 32(32位整数)。

•dateCreated, Date(日期)。

•itemKey, String(字符串)。

•thumbnail, Transformable(可变类型)。thumbnail的类型是UIImage,实体属性没有与之对应的类型。稍后就会介绍Transformable。

下面为BNRItem实体再增加一个实体属性。在Homepwner中,用户可以通过移动UITableView对象中的UITableViewCell对象,重新排列相应BNRItem对象的顺序。使用固化保存包含BNRItem对象的数组时,默认就能保留排列顺序。但是在使用Core Data时,因为关系数据库的表格默认不会保留行的排列位置,所以要针对每个BNRItem对象保存一个额外的位置信息。然后在获取某一组行时,根据这个实体属性(表格列)来排序。此外,也可以根据其他实体属性来排序(例如,获取所有的BNREmployee对象时可以根据lastName排序)。

为了保留BNRItem对象在UITableView对象中的位置,下面创建一个名为orderingValue的实体属性,用来记录BNRItem对象在UITableView对象中的位置。当要获取一组BNRItem对象时,可以要求Core Data(数据库)按实体属性orderingValue进行排序;当BNRItem对象在UITableView对象中的位置发生变化时,需要更新相应对象的orderingValue。下面创建该实体属性:名称是orderingValue,类型是Double。

Core Data只能存储有限的几种数据类型,并不能存储UIImage对象。因此只能将thumbnail声明为Core Data可以存储的transformable类型,Core Data会在存储或恢复transformable类型的实体属性时首先将其转换为NSData,然后再存入文件系统或恢复为Objective-C对象。为了向Core Data描述转换过程,需要创建NSValueTransformer的子类。

创建一个NSValueTransformer的子类BNRImageTransformer,打开BNRImage- Transformer.m,覆盖以下方法,实现UIImage和NSData对象之间的转换:

@implementation BNRImageTransformer

+ (Class)transformedValueClass

{

return [NSData class];

}

- (id)transformedValue:(id)value

{

if (!value) {

return nil;

}

if ([value isKindOfClass:[NSData class]]) {

return value;

}

return UIImagePNGRepresentation(value);

}

- (id)reverseTransformedValue:(id)value

{

return [UIImage imageWithData:value];

}

@end

BNRImageTransformer的实现代码很容易理解。transformedValueClass是类方法,用于声明transformedValue:方法的返回类型(BNRImageTransformer的转换结果类型);Core Data在存储transformable类型的实体属性时会调用transformedValue:方法,将其转换为可以存储的类型。对于thumbnail,transformedValue:的参数类型是UIImage,返回值类型是NSData;Core Data在恢复thumbnail时会调用reverseTransformedValue:,根据文件系统中存储的NSData创建UIImage对象。有了BNRImageTransformer后,Core Data还要知道如何使用BNRImageTransformer来处理thumbnail。

打开Homepwner.xcdatamodeld,选择BNRItem实体。选中Attributes区域中的thumbnail,单击检视选择面板中的按钮,打开数据模型检视面板(data model inspector),在第二个标题为Name的文本框中填入BNRImageTransformer(见图23-4)。

图23-4 为BNRItem实体增加并设置实体属性

完成上述操作后,就可以通过新创建的模型文件存取BNRItem对象了,但这还不够。使用Core Data的另一个好处是能够在实体之间建立关联。下面要增加一个名为BNRAssetType的实体,用来描述BNRItem对象的分类。例如,可以将一幅画归在艺术类。BNRAssetType和BNRItem一样,应该在同一个模型文件中创建,和BNRAssetType相对应的表格行也会在运行时被映射为相应的Objective-C对象。

在Homepwner.xcdatamodeld中增加一个名为BNRAssetType的实体,然后为这个新增加的实体创建一个名为label的实体属性,类型为String,用来代表分类的名称(见图23-5)。

图23-5 创建BNRAssetType实体

下面为BNRAssetType和BNRItem建立关系(relationship)(Core Data会将实体间的关系表示为对象之间的指针)。Core Data支持两类关系:一对一(to-one)关系和一对多(to-many)关系。当某个实体拥有某种一对一关系时,该实体的对象会有一个指针指向位于相应关系另一端的实体对象。以BNRItem为例,它会有一个指向BNRAssetType实体的一对一关系,因此BNRItem对象会拥有一个指向BNRAssetType对象的指针。

当某个实体拥有某种一对多关系时,该实体的对象会拥有一个指向NSSet对象的指针,该对象会包含和相应一对多关系有关的实体对象。以BNRAssetType为例,因为可以有多个BNRItem对象属于同一个类型(BNRAssetType),所以BNRAssetType实体应该有一个指向BNRItem实体的一对多关系。

为BNRItem实体和BNRAssetType实体创建上述关系后,就可以向某个BNRAssetType对象查询并返回一组属于该类型的BNRItem对象。此外,也可以向某个BNRItem对象查询其所属的类型(见图23-6)。

图23-6 Homepwner中的实体关系

下面要在模型文件中增加上述关系。选中BNRAssetType实体,单击Relationships(关系)列表下方的+按钮。在Relationship列中,将关系命名为items。然后在Destination(目标)列中选择BNRItem。最后在数据模型检视面板中点击标题为Type的下拉菜单,由To One改为To Many(见图23-7)。

图23-7 创建items关系

选中BNRItem实体,增加一个名为assetType的关系,然后在Destination列中选择BNRAssetType。在Inverse(反向)列中选择items(见图23-8)。

图23-8 创建assetType关系

NSManagedObject与NSManagedObject子类

通过Core Data取回(fetch)的对象,默认情况下都是NSManagedObject对象。NSManagedObject是NSObject的子类,也是Core Data的重要组成部分。NSManagedObject对象的工作模式有点类似字典对象:它会根据相应实体的每一个property(属性或关系),保存一个键-值对。

NSManagedObject对象不仅仅是数据容器。除了保存数据,还可以通过创建NSManagedObject子类让自定义的NSManagedObject对象完成其他任务。当对应某个实体的NSManagedObject对象要执行自定义的任务时,必须先创建相应的NSManagedObject子类,然后在模型文件中修改这个实体,将代表该实体的类从默认的NSManagedObject改为新创建的NSManagedObject子类。

选中BNRItem实体,打开数据模型检视面板,将Class文本框的内容修改为BNRItem,如图23-9所示。这样,当Homepwner通过Core Data取回BNRItem实体时,相应的对象类型将是BNRItem(BNRAssetType实体的类型仍是NSManagedObject)。

图23-9 修改实体的类

在为BNRItem实体创建NSManagedObject子类时,需要先解决一个问题:Homepwner项目已经有了一个名为BNRItem的类,并且这个类不是继承自NSManagedObject。如果直接修改现有的BNRItem类,则需要做很多改动才能将其父类改为NSManagedObject。最简单的解决方案是移除现有的BNRItem类,然后通过Xcode自动生成针对BNRItem实体的BNRItem类,最后将之前在BNRItem类中实现的方法拷贝至新的BNRItem类。

在Finder中,将BNRItem.h和BNRItem.m拖曳至桌面留作备份。然后在项目导航面板中移除这两个文件(在Finder中移动这两个文件后,Xcode会因为找不到相应的文件而在项目导航面板中将这两个文件显示为红色)。

选中Homepwner.xcdatamodeld,选中BNRItem实体。选择File菜单中的New菜单项,然后选择New File…选中窗口左侧iOS部分的Core Data,然后选中窗口右侧的NSManagedObject subclass,单击Next按钮。Homepwner数据模型文件之前的选择框应该已经选中,如果没有选中,请选中并单击Next按钮。在下一个窗口,确保选中了BNRItem实体并再次单击Next按钮,最后单击Save按钮创建NSManagedObject子类。

Xcode会生成两个新文件:BNRItem.h和BNRItem.m。在BNRItem.h中,将thumbnail属性的类型改为UIImage指针,并加入两个曾在BNRItem类中实现过的方法。默认情况下,Xcode会将属性声明为对象,因此int类型的属性被声明为NSNumber。将orderingValue改为double,valueInDollars改为int。代码如下:

#import <Foundation/Foundation.h>

@import CoreData;

@interface BNRItem : NSManagedObject

@property (nonatomic, strong) NSDate * dateCreated;

@property (nonatomic, strong) NSString * itemKey;

@property (nonatomic, strong) NSString * itemName;

@property (nonatomic, strong) NSNumber * orderingValue;

@property (nonatomic) double orderingValue;

@property (nonatomic, strong) NSString * serialNumber;

@property (nonatomic, strong) id thumbnail;

@property (nonatomic, strong) UIImage *thumbnail;

@property (nonatomic, strong) NSData * thumbnailData;

@property (nonatomic, strong) NSNumber * valueInDollars;

@property (nonatomic) int valueInDollars;

@property (nonatomic, strong) NSManagedObject *assetType;

- (void)setThumbnailFromImage:(UIImage *)image;

@end

(Xcode可能会将strong属性生成为retain。两者的含义是相同的,在引入ARC之前,强引用属性使用retain表示,引入之后才改为使用strong。Xcode中用于生成NSManagedObject subclass的工具可能没有同步更新,因此在生成的代码中仍然使用旧的表示方法)。

将setThumbnailFromImage:从之前的BNRItem.m拷贝至新的BNRItem.m,代码如下:

- (void)setThumbnailFromImage:(UIImage *)image

{

CGSize origImageSize = image.size;

CGRect newRect = CGRectMake(0, 0, 40, 40);

float ratio = MAX(newRect.size.width / origImageSize.width,

newRect.size.height / origImageSize.height);

UIGraphicsBeginImageContextWithOptions(newRect.size, NO, 0.0);

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:newRect

cornerRadius:5.0];

[path addClip];

CGRect projectRect;

projectRect.size.width = ratio * origImageSize.width;

projectRect.size.height = ratio * origImageSize.height;

projectRect.origin.x = (newRect.size.width - projectRect.size.width) / 2.0;

projectRect.origin.y = (newRect.size.height - projectRect.size.height) / 2.0;

[image drawInRect:projectRect];

UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext();

self.thumbnail = smallImage;

UIGraphicsEndImageContext();

}

首次启动Homepwner时,肯定不会有已存的BNRItem对象和BNRAssetType对象。当用户创建新的BNRItem对象时,Homepwner会将新增加的对象加入数据库。当这些对象被加入数据库时,都会收到awakeFromInsert消息。所以,应该在BNRItem对象的awakeFromInsert方法中设置dateCreated和itemKey属性。在BNRItem.m中实现awakeFromInsert方法,代码如下:

- (void)awakeFromInsert

{

[super awakeFromInsert];

self.dateCreated = [NSDate date];

// 创建NSUUID对象,获取其UUID字符串

NSUUID *uuid = [[NSUUID alloc] init];

NSString *key = [uuid UUIDString];

self.itemKey = key;

}

这段代码的功能和前面BNRItem的指定初始化方法相同。构建应用,检查语法错误,先不要运行。

更新BNRItemStore

Core Data框架中的NSManagedObjectContext负责应用和数据库之间的交互工作。通过NSManagedObjectContext对象所使用的NSPersistentStoreCoordinator对象,可以指定文件路径并打开相应的SQLite数据库。NSPersistentStoreCoordinator对象需要配合某个模型文件才能工作(NSManagedObjectModel对象可以代表模型文件)。在Homepwner中,需要由BNRItemStore对象来使用上述的多个Core Data对象,以完成数据的存取工作。这些对象之间的关系如图23-10所示。

图23-10 BNRItemStore与NSManagedObjectContext

在BNRItemStore.m中导入Core Data框架并在类扩展中声明三个属性,代码如下:

@import CoreData;

@interface BNRItemStore ()

@property (nonatomic) NSMutableArray *privateItems;

@property (nonatomic, strong) NSMutableArray *allAssetTypes;

@property (nonatomic, strong) NSManagedObjectContext *context;

@property (nonatomic, strong) NSManagedObjectModel *model;

然后修改itemArchivePath方法,返回不同的路径,Core Data能够将数据保存至另一个文件,代码如下:

- (NSString *)itemArchivePath

{

NSArray *documentDirectories =

NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,

NSUserDomainMask,

YES);

// 从documentDirectories数组获取文档目录路径(该数组只包含一个对象)

NSString *documentDirectory = [documentDirectories firstObject];

return [documentDirectory stringByAppendingPathComponent:@“items.archive”];

return [documentDirectory stringByAppendingPathComponent:@“store.data”];

}

初始化BNRItemStore对象时,需要创建并设置相应的NSManagedObjectContext对象和NSPersistentStoreCoordinator对象。NSPersistentStoreCoordinator对象需要知道两件事情:实体信息(包括实体属性和关系)和存取数据的路径。为此,要先创建一个NSManagedObjectModel对象,保存源自Homepwner.xcdatamodeld的实体信息。然后用新创建的NSManagedObjectModel对象初始化NSPersistentStoreCoordinator对象。创建并设置NSPersistentStoreCoordinator对象后,就可以创建一个NSManagedObject- Context对象,并传入之前创建的NSPersistentStoreCoordinator对象(由该对象负责文件的存取)。

更新BNRItemStore.m中的initPrivate方法,代码如下:

- (instancetype)initPrivate

{

self = [super init];

if (self) {

NSString *path = self.itemArchivePath;

_privateItems = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

if (!_privateItems) {

_privateItems = [[NSMutableArray alloc] init];

}

// 读取Homepwner.xcdatamodeld

_model = [NSManagedObjectModel mergedModelFromBundles:nil];

NSPersistentStoreCoordinator *psc =

[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_model];

// 设置SQLite文件路径

NSString *path = self.itemArchivePath;

NSURL *storeURL = [NSURL fileURLWithPath:path];

NSError *error = nil;

if (![psc addPersistentStoreWithType:NSSQLiteStoreType

configuration:nil

URL:storeURL

options:nil

error:&error]) {

@throw [NSException exceptionWithName:@“OpenFailure”

reason:[error localizedDescription]

userInfo:nil];

}

// 创建NSManagedObjectContext对象

_context = [[NSManagedObjectContext alloc] init];

_context.persistentStoreCoordinator = psc;

}

return self;

}

修改前的BNRItemStore对象会通过固化将整个BNRItem数组写入文件,而修改后的BNRItemStore对象可以向NSManagedObjectContext对象发送save:消息,NSManagedObjectContext对象会根据上次保存后发生的变化更新store.data中的相应记录。修改BNRItemStore.m中的saveChanges方法,代码如下:

- (BOOL)saveChanges

{

NSString *path = [self itemArchivePath];

return [NSKeyedArchiver archiveRootObject:allItems

toFile:[self itemArchivePath]];

NSError *error;

BOOL successful = [self.context save:&error];

if (!successful) {

NSLog(@“Error saving: %@”, [error localizedDescription]);

}

return successful;

}

saveChanges方法会在应用进入后台运行状态时被调用。

NSFetchRequest与NSPredicate

当Homepwner第一次使用BNRItem对象时,会一次性地取出store.data中的所有BNRItem对象。要通过NSManagedObjectContext对象得到这些BNRItem对象,就必须先设置一个NSFetchRequest对象,然后执行该对象。执行NSFetchRequest对象后,可以得到一组与指定的参数相匹配的对象。

执行NSFetchRequest对象前,需要先设置相应的实体描述。实体描述的作用是定义所要获取的对象的实体。以Homepwner为例,要取回BNRItem对象,就要将实体描述定义为BNRItem实体。此外,还可以为NSFetchRequest对象设置排序描述对象(sort descriptors),用于指定返回对象的排列次序。排序描述对象拥有一个键(和某个实体属性对应)和一个布尔值(代表次序是升序还是降序)。下面让Core Data根据BNRItem实体的orderingValue属性,按升序排列返回的BNRItem对象。在BNRItemStore.m中,定义一个新方法loadAllItems,创建并设置NSFetchRequest对象,然后执行该NSFetchRequest对象,最后将返回的结果存入privateItems,代码如下:

- (void)loadAllItems

{

if (!self.privateItems) {

NSFetchRequest *request = [[NSFetchRequest alloc] init];

NSEntityDescription *e = [NSEntityDescription entityForName:@“BNRItem”

inManagedObjectContext:self.context];

request.entity = e;

NSSortDescriptor *sd = [NSSortDescriptor

sortDescriptorWithKey:@“orderingValue”

ascending:YES];

request.sortDescriptors = @[sd];

NSError *error;

NSArray *result = [self.context executeFetchRequest:request

error:&error];

if (!result) {

[NSException raise:@“Fetch failed”

format:@“Reason: %@”, [error localizedDescription]];

}

self.privateItems = [[NSMutableArray alloc] initWithArray:result];

}

}

同时,在BNRItemStore.m的initPrivate方法末端,向BNRItemStore对象发送loadAllItems消息,代码如下:

_context.persistentStoreCoordinator = psc;

[self loadAllItems];

}

return self;

}

构建应用,检查是否有语法错误。

这样,Homepwner会一次性地取回所有的BNRItem实体对象。这是相对简单的情况,如果某个应用的数据庞大,就应该只获取需要使用的实体对象。为NSFetchRequest对象设置特定的NSPredicate对象,可以使Core Data只返回符合条件的对象。

一个NSPredicate对象可以包含一个“条件”,其结果可以是真或假。例如,如果只要获取价值大于50元的BNRItem对象,就可以创建一个NSPredicate对象并将该对象加入相应的NSFetchRequest对象,代码如下:

NSPredicate *p = [NSPredicate predicateWithFormat:@“valueInDollars > 50”];

[request setPredicate:p];

NSPredicate对象的格式字符串可以很长、很复杂。Apple的开发文档《Predicate Programming Guide》对此有详细的介绍。

此外,还可以用NSPredicate对象过滤数组中的对象。因此,即使是通过Core Data获取的allItems数组,一样可以使用NSPredicate对象再次过滤,代码如下:

NSArray *expensiveStuff = [allItems filteredArrayUsingPredicate:p];

添加和删除BNRItem对象

以上完成了保存和载入功能,下面实现添加和删除功能。改用Core Data后,就不能再用alloc方法和init方法来创建BNRItem对象,而应该通过NSManagedObjectContext对象插入一个针对BNRItem实体的新对象,并得到相应的BNRItem对象。修改BNRItemStore.m中的createItem方法,代码如下:

- (BNRItem *)createItem

{

BNRItem *item = [[BNRItem alloc] init];

double order;

if ([self.allItems count] == 0) {

order = 1.0;

} else {

order = [[self.privateItems lastObject] orderingValue] + 1.0;

}

NSLog(@“Adding after %d items, order = %.2f”,

[self.privateItems count], order);

BNRItem *item =

[NSEntityDescription insertNewObjectForEntityForName:@“BNRItem”

inManagedObjectContext:self.context];

item.orderingValue = order;

[self.privateItems addObject:item];

return item;

}

当用户删除某个BNRItem对象后,需要通知NSManagedObjectContext对象从数据库删除相应的数据。将以下代码加入BNRItemStore.m中的removeItem:。

- (void)removeItem:(BNRItem *)item

{

NSString *key = item.itemKey;

[[BNRImageStore sharedStore] deleteImageForKey:key];

[self.context deleteObject:item];

[self.privateItems removeObjectIdenticalTo:item];

}

排列BNRItem对象

下面要为BNRItemStore实现BNRItem对象的排序功能。因为Core Data在使用关系数据库保存数据时,默认不会保留行的排列位置,所以当某个BNRItem对象在UITableView对象中的位置发生变化时,就必须更新该对象的orderingValue属性。

如果orderingValue是整数类型,那么实现排序功能会有点复杂。当某个BNRItem对象移动至新位置时,其他的BNRItem对象的orderingValue属性也要跟着变化。这也是为什么之前将orderingValue的类型声明为了double。当orderingValue的类型为double时,只需要找出位于插入位置之前和之后的BNRItem对象,将两个对象的orderingValues属性相加并除以2,就可以得到移动后的新orderingValue。修改BNRItemStore.m中的movePossessionAtIndex:toIndex:,加入排序功能,代码如下:

- (void)moveItemAtIndex:(NSUInteger)fromIndex

toIndex:(NSUInteger)toIndex

{

if (fromIndex == toIndex) {

return;

}

BNRItem *item = self.privateItems[fromIndex];

[self.privateItems removeObjectAtIndex:fromIndex];

[self.privateItems insertObject:item atIndex:toIndex];

// 为移动的BNRItem对象计算新的orderValue

double lowerBound = 0.0;

// 在数组中,该对象之前是否有其他对象?

if (toIndex > 0) {

lowerBound = [self.privateItems[(toIndex - 1)] orderingValue];

} else {

lowerBound = [self.privateItems[1] orderingValue] - 2.0;

}

double upperBound = 0.0;

// 在数组中,该对象之后是否有其他对象?

if (toIndex < [self.privateItems count] - 1) {

upperBound = [self.privateItems[(toIndex + 1)] orderingValue];

} else {

upperBound = [self.privateItems[(toIndex - 1)] orderingValue] + 2.0;

}

double newOrderValue = (lowerBound + upperBound) / 2.0;

NSLog(@“moving to order %f”, newOrderValue);

item.orderingValue = newOrderValue;

}

构建并运行应用。虽然Homepwner的功能没有发生变化,但是内部已经改用Core Data实现数据的存取。

为Homepwner增加BNRAssetType功能

除了BNRItem实体,Homepwner的模型文件还描述了一个名为BNRAssetType的实体。BNRItem实体和BNRAssetType实体之间是一对一的关系。Homepwner需要提供某种途径,使用户可以为某个BNRItem对象设置BNRAssetType对象。此外,还要扩充BNRItemStore,使BNRItemStore对象能够获取BNRAssetType对象(创建此对象的任务将作为练习留给读者完成)。

在BNRItemStore.h中声明一个新方法,代码如下:

- (NSArray *)allAssetTypes;

在BNRItemStore.m中实现该方法,当Homepwner首次运行时(这时的BNRItemStore对象不会包含任何BNRAssetType对象),需要创建三种默认类型,代码如下:

- (NSArray *)allAssetTypes

{

if (!_allAssetTypes) {

NSFetchRequest *request = [[NSFetchRequest alloc] init];

NSEntityDescription *e =

[NSEntityDescription entityForName:@“BNRAssetType”

inManagedObjectContext:self.context];

request.entity = e;

NSError *error = nil;

NSArray *result = [self.context executeFetchRequest:request

error:&error];

if (!result) {

[NSException raise:@“Fetch failed”

format:@“Reason: %@”, [error localizedDescription]];

}

_allAssetTypes = [result mutableCopy];

}

// 第一次运行?

if ([_allAssetTypes count] == 0) {

NSManagedObject *type;

type = [NSEntityDescription

insertNewObjectForEntityForName:@“BNRAssetType”

inManagedObjectContext:self.context];

[type setValue:@“Furniture” forKey:@“label”];

[_allAssetTypes addObject:type];

type = [NSEntityDescription

insertNewObjectForEntityForName:@“BNRAssetType”

inManagedObjectContext:self.context];

[type setValue:@“Jewelry” forKey:@“label”];

[_allAssetTypes addObject:type];

type = [NSEntityDescription

insertNewObjectForEntityForName:@“BNRAssetType”

inManagedObjectContext:self.context];

[type setValue:@“Electronics” forKey:@“label”];

[_allAssetTypes addObject:type];

}

return _allAssetTypes;

}

下面要修改用户界面,使用户能够在BNRDetailViewController对象的视图中查看某个BNRItem对象的BNRAssetType对象,并进行修改(见图23-11)。

图23-11 查看和修改BNRAssetType对象的界面

使用Objective-C class模板创建新文件,父类选择NSObject,类名使用BNRAssetType- ViewController。

在BNRAssetTypeViewController.h中,先前置声明BNRItem类,然后将BNRAssetTypeViewController的父类改为UITableViewController,最后为BNRAssetTypeViewController声明一个类型为BNRItem的item属性。

#import <Foundation/Foundation.h>

@class BNRItem;

@interface BNRAssetTypeViewController : NSObject

@interface BNRAssetTypeViewController : UITableViewController

@property (nonatomic, strong) BNRItem *item;

@end

BNRAssetTypeViewController对象的作用是显示一组可供用户选择的BNRAssetType对象。按下BNRDetailViewController中的指定按钮,Homepwner应该会显示BNRAssetTypeViewController对象。在BNRAssetTypeViewController.m中实现数据源方法并导入相应的头文件(前文已经介绍过如何实现这类代码):

#import “BNRAssetTypeViewController.h”

#import “BNRItemStore.h”

#import “BNRItem.h”

@implementation BNRAssetTypeViewController

- (instancetype)init

{

return [super initWithStyle:UITableViewStylePlain];

}

- (instancetype)initWithStyle:(UITableViewStyle)style

{

return [self init];

}

- (void)viewDidLoad

{

[super viewDidLoad];

[self.tableView registerClass:[UITableViewCell class]

forCellReuseIdentifier:@“UITableViewCell”];

}

- (NSInteger)tableView:(UITableView *)tableView

numberOfRowsInSection:(NSInteger)section

{

return [[[BNRItemStore sharedStore] allAssetTypes] count];

}

- (UITableViewCell *)tableView:(UITableView *)tableView

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

UITableViewCell *cell =

[tableView dequeueReusableCellWithIdentifier:@“UITableViewCell”

forIndexPath:indexPath];

NSArray *allAssets = [[BNRItemStore sharedStore] allAssetTypes];

NSManagedObject *assetType = allAssets[indexPath.row];

// 通过键-值编码(key-value coding)得到BNRAssetType对象的label属性

NSString *assetLabel = [assetType valueForKey:@“label”];

[cell.textLabel.text = assetLabel;

// 为当前选中的对象加上勾选标记

if (assetType == self.item.assetType) {

cell.accessoryType = UITableViewCellAccessoryCheckmark;

} else {

cell.accessoryType = UITableViewCellAccessoryNone;

}

return cell;

}

- (void)tableView:(UITableView *)tableView

didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];

cell.accessoryType = UITableViewCellAccessoryCheckmark;

NSArray *allAssets = [[BNRItemStore sharedStore] allAssetTypes];

NSManagedObject *assetType = allAssets[indexPath.row];

self.item.assetType = assetType;

[self.navigationController popViewControllerAnimated:YES];

}

@end

打开BNRDetailViewController.xib,向UIToolbar上拖曳一个UIBarButtonItem,然后按住Control将其拖曳到BNRDetailViewController.m的类扩展中,创建名为assetTypeButton插座变量。接下来使用同样的方法为assetTypeButton创建动作方法,命名为showAssetTypePicker:。

现在BNRDetailViewController.m中应该已经声明了以下属性和方法:

@property (weak, nonatomic) IBOutlet UIBarButtonItem *assetTypeButton;

@end

@implementation BNRDetailViewController

// 省略其他方法

- (IBAction)showAssetTypePicker:(id)sender

{

}

@end

在BNRDetailViewController.m顶部导入BNRAssetTypeViewController.h,代码如下:

#import “BNRDetailViewController.h”

#import “BNRAssetTypeViewController.h”

在BNRDetailViewController.m中实现showAssetTypePicker:,代码如下:

- (IBAction)showAssetTypePicker:(id)sender

{

[self.view endEditing:YES];

BNRAssetTypeViewController *avc = [[BNRAssetTypeViewController

alloc] init]; avc.item = self.item;

[self.navigationController pushViewController:avc

animated:YES];

}

最后更新UIBarButtonItem对象的标题,显示指定的BNRItem对象的类型。将以下代码加入BNRDetailViewController.m中的viewWillAppear:。

if (self.itemKey) {

// 根据itemKey从BNRImageStore对象获取相应的图片

UIImage *imageToDisplay = [[BNRImageStore sharedStore]

imageForKey:self.itemKey];

// 将得到的图片赋给UIImageView对象

self.imageView.image = imageToDisplay;

} else {

// 清空UIImageView对象

self.imageView.image = nil;

}

NSString *typeLabel = [self.item.assetType valueForKey:@“label”];

if (!typeLabel) {

typeLabel = @“None”;

}

self.assetTypeButton.title = [NSString stringWithFormat:@“Type: %@”, typeLabel];

[self updateFonts];

}

构建并运行应用。选中某个BNRItem对象后,应该能设置该对象的BNRAssetType对象。