【问题】
在折腾:
给Your Second iOS App:BirdWatching添加支持程序退出后,用户数据仍然保留
的过程中,遇到一个问题,需要将一个自定义对象的数组,保存到NSUserDefaults。
【解决过程】
1.经过学习很多资料后,然后加上一番折腾,先去实现了单个自定义对象的编解码和存储/恢复:
贴出部分相关的代码:
BirdSighting.h:
@interface BirdSighting : NSObject <NSCoding> { } @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *location; @property (nonatomic, strong) NSDate *date; @property (nonatomic) UIImage *image;
BirdSighting.m:
-(void)encodeWithCoder:(NSCoder *)aCoder{ //encode properties/values [aCoder encodeObject:self.name forKey:@"name"]; [aCoder encodeObject:self.location forKey:@"location"]; [aCoder encodeObject:self.date forKey:@"date"]; [aCoder encodeObject:self.image forKey:@"image"]; } -(id)initWithCoder:(NSCoder *)aDecoder{ if((self = [super init])) { //decode properties/values self.name = [aDecoder decodeObjectForKey:@"name"]; self.location = [aDecoder decodeObjectForKey:@"location"]; self.date = [aDecoder decodeObjectForKey:@"date"]; self.image = [aDecoder decodeObjectForKey:@"image"]; } return self; }
BirdSightingDataController.m:
-(void)initializeDefaultDataList{ NSMutableArray *sightingList = [[NSMutableArray alloc] init]; self.masterBirdSightingList = sightingList; //[self addBirdSightingWithName:@"Pigeon" location:@"Everywhere" date:[NSDate date] image:[UIImage imageNamed:@"defaultBirdImage.gif"] ]; //restore data // NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // NSData *savedEncodedData = [defaults objectForKey:@"BirdSightingList"]; // self.masterBirdSightingList = (NSMutableArray *)[NSKeyedUnarchiver unarchiveObjectWithData:savedEncodedData]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSData *prevSavedData = [defaults objectForKey:@"SingleBirdSightingObject"]; BirdSighting *decodedSingleObj = (BirdSighting *)[NSKeyedUnarchiver unarchiveObjectWithData:prevSavedData]; if(decodedSingleObj != nil) { [self.masterBirdSightingList addObject:decodedSingleObj]; } } ... -(void)addBirdSightingWithName:(NSString *)inputBirdName location:(NSString *)inputLocation date:(NSDate *)date image:(UIImage *)image{ BirdSighting *sighting; //NSDate *today = [NSDate date]; //sighting = [[BirdSighting alloc]initWithName:inputBirdName location:inputLocation date:today]; sighting = [[BirdSighting alloc]initWithName:inputBirdName location:inputLocation date:date image:image]; [self.masterBirdSightingList addObject:sighting]; //save data //NSData *encodedCurBirdSightingList = [NSKeyedArchiver archivedDataWithRootObject:self.masterBirdSightingList]; // NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // [defaults setObject:encodedCurBirdSightingList forKey:@"BirdSightingList"]; NSData *encodedSingleObj = [NSKeyedArchiver archivedDataWithRootObject:sighting]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setObject:encodedSingleObj forKey:@"SingleBirdSightingObject"]; }
如此,暂时可以实现了,单个的对象,每次保存后,下次恢复,也可以完整的恢复出来数据并正确显示:
2.后来也参考了很多帖子:
How to store custom objects in NSUserDefaults
save and restore an array of custom objects
How to archive an NSArray of custom objects to file in Objective-C
3.但是我这里又遇到一个问题,那就是,我此处需要保存的是自定义对象的数组,即用一个数组指针指向的多个自定义对象,所以,就没法直接使用上述方法保存这一系列自定义对象了,所以还要想其他办法才行。
但是后来却发现,其实上述原先的代码,本身已经可以工作了。
即,解码的时候,可以看到能从NSUserDefaults获得一堆的数据了:
对应的,在恢复自定义对象数组masterBirdSightingList之前,其是null:
然后调用unarchiveObjectWithData时,也可以执行对应的自定义对象的initWithCoder函数了:
解码后,对应的masterBirdSightingList就包含了还原出来的2个对象了:
而编码过程也是所期望的:
先是NSData是空的:
然后调用archivedDataWithRootObject去编码,可以调用到自定义对象的encodeWithCoder函数:
然后经过4个对象的编码后,就包含了编码后的数据了:
接着就是正常的保存到NSUserDefaults中去了.
如此,关闭程序之前的显示的数据是:
恢复后的显示的效果为:
很明显,两者完全一样,即整套逻辑是正常工作的,可以完美的实现自定义对象的数组的保存和恢复.
下面对整个逻辑和示例代码,进行详细的总结。
【总结】
【自定义对象的数组NSMutableArray,保存到/恢复自 NSUserDefaults 的逻辑】
想要实现自定义类型的对象的数组,保存到NSUserDefaults中去,核心的逻辑是:
1.想要把数据保存到NSUserDefaults中的话,那首先要知道NSUserDefaults所支持的数据类型。
根据官网
NSUserDefaults Class Reference
的解释,默认支持类型是:
- NSData
- NSString
- NSNumber
- NSDate
- NSArray
- NSDictionary
所以,如果你本身要保存的数据是属于上述中已有的,常见的,数据类型,比如NSString,NSDate之类的话,那么很简单,直接保存即可。
2.而此处所谓的自定义对象(Custom Object)/类(Class)的话,常见的做法是,把自定义对象转换为NSData,然后利用NSUserDefaults去保存NSData。
把自定义对象转换为NSData,用的方法就是Archive,Archive可翻译为归档,压缩。
相关的方法是:
- NSKeyedArchiver Class Reference 的
+ archivedDataWithRootObject:
- NSKeyedUnarchiver Class Reference 的
+ unarchiveObjectWithData:
对应的,自定义对象本身,需要遵循NSCoding,其有两个函数:
3. 之后:
- 当你的使用NSKeyedArchiver去把自定义对象编码为NSData时,就会调用到该对象的encodeWithCoder,编码后得到了NSData后,再保存到NSUserDefaults里面;
- 当你从NSUserDefaults读取出你之前所保存的NSData之后,再调用NSKeyedUnarchiver去解码此NSData,然后内部会自动调用到该自定义对象的initWithCoder方法,将NSData解码为对应的自己的对象
如此,就可以实现将自定义对象保存到NSUserDefaults和从NSUserDefaults恢复之前保存的自定义对象了。
4.上述方法,看似是只针对单个自定义对象的,但是本质上,对于自定义对象的数组,也是适用的。
因为,对于一般的NSMutableArray来说,其本身也是实现了NSCoding,所以,当你去使用NSKeyedArchiver将一个自定义对象的NSMutableArray数组编码为NSData时,其内部会先调用NSMutableArray的encodeWithCoder,然后发现是个自定义对象的数组,然后就会分别针对每个自定义对象调用该自定义对象的encodeWithCoder,这样,最终实现了将整个自定义对象数组,转换为NSData;
相应的解码过程也是一样的,当从NSData解码为对应的自定义对象的NSMutableArray数组时,也是先调用NSMutableArray的initWithCoder,然后分别调用每个自定义对象的initWithCoder,最终解码为对应的自定义对象的NSMutableArray数组。
【自定义对象的数组NSMutableArray,保存到/恢复自 NSUserDefaults 的 参考代码】
1. 自定义对象BirdSighting
(1)对应的头文件中:
A. 实现了遵循NSCoding(Conform to NSCoding)
B. 包含了几个自己的不同类型的属性
// // BirdSighting.h // BirdWatching // // Created by li crifan on 12-8-20. // Copyright (c) 2012年 li crifan. All rights reserved. // #import <Foundation/Foundation.h> @interface BirdSighting : NSObject <NSCoding> { } @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *location; @property (nonatomic, strong) NSDate *date; @property (nonatomic) UIImage *image; @end
(2)对应BirdSighting.m中
A。实现了对应的NSCoding的encodeWithCoder,将对应的不同的属性变量编码为对应数据
B。实现了对应的NSCoding的initWithCoder,从对应的 key中解码出数据还原到对应的属性变量中
// // BirdSighting.m // BirdWatching // // Created by li crifan on 12-8-20. // Copyright (c) 2012年 li crifan. All rights reserved. // #import "BirdSighting.h" @implementation BirdSighting ... -(void)encodeWithCoder:(NSCoder *)aCoder{ //encode properties/values [aCoder encodeObject:self.name forKey:@"name"]; [aCoder encodeObject:self.location forKey:@"location"]; [aCoder encodeObject:self.date forKey:@"date"]; [aCoder encodeObject:self.image forKey:@"image"]; } -(id)initWithCoder:(NSCoder *)aDecoder{ if((self = [super init])) { //decode properties/values self.name = [aDecoder decodeObjectForKey:@"name"]; self.location = [aDecoder decodeObjectForKey:@"location"]; self.date = [aDecoder decodeObjectForKey:@"date"]; self.image = [aDecoder decodeObjectForKey:@"image"]; } return self; } @end
2. 在别处将自定义对象BirdSighting的数组保存到和读取自NSUserDefaults
先说背景条件:
BirdSightingDataController是个Controller
在对应的头文件BirdSightingDataController.h中,定义了个属性变量,是 自定义对象BirdSighting的数组 NSMutableArray
// // BirdSightingDataController.h // BirdWatching // // Created by li crifan on 12-8-21. // Copyright (c) 2012年 li crifan. All rights reserved. // #import <Foundation/Foundation.h> @class BirdSighting; @interface BirdSightingDataController : NSObject { } @property (nonatomic, copy)NSMutableArray *masterBirdSightingList; ...... @end
然后想要在Controller的实现文件BirdSightingDataController.m中,保存这个 自定义对象BirdSighting的数组NSMutableArray, 即属性变量masterBirdSightingList 到 NSUserDefaults,
以及对应的也可以从NSUserDefaults中还原出masterBirdSightingList。
(1)编码 masterBirdSightingList(自定义对象BirdSighting的数组NSMutableArray) 到 NSUserDefaults 中
-(void)addBirdSightingWithName:(NSString *)inputBirdName location:(NSString *)inputLocation date:(NSDate *)date image:(UIImage *)image{ BirdSighting *sighting; sighting = [[BirdSighting alloc]initWithName:inputBirdName location:inputLocation date:date image:image]; [self.masterBirdSightingList addObject:sighting]; //save data NSData *encodedCurBirdSightingList = [NSKeyedArchiver archivedDataWithRootObject:self.masterBirdSightingList]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setObject:encodedCurBirdSightingList forKey:@"BirdSightingList"]; }
(2) 从 NSUserDefaults 中 解码还原出 (自定义对象BirdSighting的数组NSMutableArray)masterBirdSightingList
-(void)initializeDefaultDataList{ //restore data NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSData *savedEncodedData = [defaults objectForKey:@"BirdSightingList"]; if(savedEncodedData == nil) { NSMutableArray *sightingList = [[NSMutableArray alloc] init]; self.masterBirdSightingList = sightingList; } else{ self.masterBirdSightingList = (NSMutableArray *)[NSKeyedUnarchiver unarchiveObjectWithData:savedEncodedData]; } }
其中,两者的key要一致,即此处两处都使用同一个"BirdSightingList"。
如此,就可以实现,将 自定义对象的数组 保存到 NSUserDefaults 了。
特别提示:
如果你遇到数据偶尔没有保存成功的话,那和你要保存的变量,以及NSCoder对象是否是autoRelease的,没有半毛钱关系,而是和iOS中数据同步写回有关系。
解决办法参见:
【已解决】NSUserDefaults偶尔/有时候保存数据会失败/失效
转载请注明:在路上 » 【已解决】iPhone/iOS中保存自定义对象(Custom Object/Custom Class)的数组(NSMutableArray/NSArray)到NSUserDefaults