NSCoderによるデータ保存

2010/07/05

NSCoder、NSZone、NSKeyedArchiverを使って、データの保存をしてみます。

まず、IBで適当にフィールドを用意し、アウトレットも接続しておきます。

サンプル

#define kFileName @"archive"
#define kDataKey @"Data"

@interface PersistenceViewController : UIViewController {
    UITextField *firstNameField;
    UITextField *lastNameField;
    UITextField *passwordField;
}

@property (nonatomic, retain) IBOutlet UITextField *firstNameField;
@property (nonatomic, retain) IBOutlet UITextField *lastNameField;
@property (nonatomic, retain) IBOutlet UITextField *passwordField;

-(NSString *)dataFilePath;
-(void)applicationWillTerminate:(NSNotification *)notification;

kFileNameは保存ファイル名、kDataKeyはデータ保存のキーです。

カスタムメソッドとして、データ保存ファイルのパス取得「dataFilePath」と、 NSNotification(監視)を利用してデータ保存をするメソッドを定義し、.mファイルに実装します。

-(NSString *)dataFilePath {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(
                                                         NSDocumentDirectory, 
                                                         NSUserDomainMask, 
                                                          YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    return [documentsDirectory stringByAppendingFormat:@"/%@", kFileName];
}
-(void)applicationWillTerminate:(NSNotification *)notification {
    Users *users = [[Users alloc] init];
    users.firstName = firstNameField.text;
    users.lastName = lastNameField.text;
    users.password = passwordField.text;
    
    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
                                 initForWritingWithMutableData:data];
    [archiver encodeObject:users forKey:kDataKey];
    [archiver finishEncoding];
    [data writeToFile:[self dataFilePath] atomically:YES];
    [users release];
    [archiver release];
    [data release];
}

参考本を見てやってたのですが、どうやらstringByAppendingFormatでパスを作る時に、「/」をつけておかないと絶対パスが生成できませんでした。 ただし、シミュレータレベルなので実機の挙動はわかりません。。。

ただ、ログを見る限り「Documentsarchive」となってしまっていたので、多分「/」をつけないとダメだと思います。

次に、データのモデル的なクラスを作ります。

#define kFirstName @"firstName"
#define kLastName @"lastName"
#define kPassword @"password"

@interface Users : NSObject<NSCoding, NSCopying> {
    NSString *firstName;
    NSString *lastName;
    NSString *password;
}

@property (nonatomic, retain) NSString *firstName;
@property (nonatomic, retain) NSString *lastName;
@property (nonatomic, retain) NSString *password;

ファイルの読み書きはNSCoding、NSCopyingを利用するので拡張しておきます。

@interface Users : NSObject<NSCoding, NSCopying>

実装は以下の通り

@implementation Users
@synthesize firstName;
@synthesize lastName;
@synthesize password;

- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:firstName forKey:kFirstName];
    [encoder encodeObject:lastName forKey:kLastName];
    [encoder encodeObject:password forKey:kPassword];
}
- (id)initWithCoder:(NSCoder *)decoder {
    if (self = [super init]) {
        self.firstName = [decoder decodeObjectForKey:kFirstName];
        self.lastName = [decoder decodeObjectForKey:kLastName];
        self.password = [decoder decodeObjectForKey:kPassword];
    }
    return self;
}
- (id)copyWithZone:(NSZone *)zone {
    NSLog(@"copyWithZone");
    Users *copy = [[[self class] allocWithZone:zone] init];
    copy.firstName = [[self.firstName copyWithZone:zone] autorelease];
    copy.lastName = [[self.lastName copyWithZone:zone] autorelease];
    copy.password = [[self.password copyWithZone:zone] autorelease];
    return copy;
}

initWithCoder、encodeWithCoder、encodeWithCoderは必ず実装が必要なプロトコルです。 initWithCoderで読み込み、encodeWithCoderで書き込みです。 copyWithZoneは、現在のデータをコピーしておくことで、データ保存処理を円滑にしてくれます。 autoreleaseを設定しておく事で、自動的にメモり解放してくれます。

しかし、全部覚えなくてもコードヒントで入力できるので便利ですね(^^;)

最後に、viewDidLoadによるデータ読み込みとデータ書き込みの監視を実装します。

- (void)viewDidLoad {
    NSString *filePath = [self dataFilePath];
    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        NSData *data = [[NSMutableData alloc]
                        initWithContentsOfFile:[self dataFilePath]];
        NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
                                       initForReadingWithData:data];
        Users *users = [unarchiver decodeObjectForKey:kDataKey];
        [unarchiver finishDecoding];
        
        firstNameField.text = users.firstName;
        lastNameField.text = users.lastName;
        passwordField.text = users.password;
        
        [unarchiver release];
        [data release];
    }
         
    UIApplication *app = [UIApplication sharedApplication];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                        selector:@selector(applicationWillTerminate:)
                                        name:UIApplicationWillTerminateNotification
                                        object:app];
    [super viewDidLoad];
}

まずはファイルの読み込み ファイルの有無は[[NSFileManager defaultManager] fileExistsAtPath]で判断。 ファイルがあれば、[NSMutableData initWithContentsOfFile:]で指定のファイルパスからデータ(NSData)を取得します。

[NSKeyedUnarchiver initForReadingWithData]でNSDataをNSKeyedUnarchiverに変換し、 [NSKeyedUnarchiver decodeObjectForKey:]でデコードします。 キャストしなくてもUsers型でデータをバインドできるのが素晴らしい。

次にファイルの書き込みの監視登録です。 [UIApplication sharedApplication]でアプリケーションを取得できます。 NSNotificationCenterでUIApplicationのメソッドが発生した時にイベントを発生させます。

この例では、[UIApplication UIApplicationWillTerminateNotification]が発生したときに、applicationWillTerminateを実行します。 addObserverに自分自身、objectに監視対象のUIApplication、@selectorは発生後のメソッドです。

ここら辺りになってくると、UIApplicationの遷移を理解しておかないと思わぬ挙動になりそうなので注意ですね(^^;)