Apple Docs から: NSUserDefaults から返される値は、可変オブジェクトを値として設定しても不変です。たとえば、変更可能な文字列を "MyStringDefault" の値として設定すると、後で stringForKey: を使用して取得する文字列は不変になります。
Appleのドキュメントは常にこれを言っています。実際には、同期を使用している限り、Apple の警告にもかかわらず、ディクショナリと配列は常に変更可能です。Mountain Lion の違いは、深くネストされた辞書を読み書きする場合、それらの深くネストされたオブジェクトが NSUserDefaults に保存されないことです。
アプリを終了する直前に値を読み取ることができるため、保存されたように見えることさえあります。微妙に、再起動するとそれらは表示されません。
さらに悪いことに、mutableCopy を作成しても問題は解決しません。mutableCopyDeepPropertyList を作成するだけで問題が解決します (以下の解決策を参照)。
Mountain Lion の前は、このようなコードは機能していましたが、ドキュメントではそうすべきではないと示唆されていました。
NSMutableDictionary *parentDict = [[NSUserDefaults standardUserDefaults] objectForKey:@"parentDict"];
NSLog( @"starting up... %@", parentDict );
if ( !parentDict )
{
NSMutableDictionary *childDict = [NSMutableDictionary dictionaryWithObject: @"1" forKey: @"MyNumber1"];
parentDict = [NSMutableDictionary dictionaryWithObject:childDict forKey: @"childDict"];
[[NSUserDefaults standardUserDefaults] setObject: parentDict forKey: @"parentDict"];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog( @"first time run... %@", parentDict );
exit(0);
}
NSMutableDictionary *childDict = [parentDict objectForKey: @"childDict"];
[childDict removeObjectForKey:@"MyNumber2"];
[childDict setObject: @"2" forKey: @"MyNumber2"];
[[NSUserDefaults standardUserDefaults] setObject: parentDict forKey: @"parentDict"];
[[NSUserDefaults standardUserDefaults] synchronize];
// Now read the value back to verify it:
parentDict = [[NSUserDefaults standardUserDefaults] objectForKey:@"parentDict"];
NSLog( @"exiting... %@", parentDict );
exit(0);
1 回目の実行:
2013-07-26 18:01:55.064 Mbox Director-[デバッグ][15391:303] 起動中... (null) 2013-07-26 18:01:55.210 Mbox Director-[デバッグ][15391:303]初めて実行... { childDict = { MyNumber1 = 1; }; }
2回目の実行(すべてが正しいように見えます):
2013-07-26 18:02:54.999 Mbox Director-[デバッグ][15510:303] 起動中... { childDict = { MyNumber1 = 1; }; } 2013-07-26 18:02:55.000 Mbox Director-[デバッグ][15510:303] 終了中... { childDict = { MyNumber1 = 1; マイナンバー2 = 2; }; }
Mountain Lion の 3 回目の実行結果 (通知、起動時に MyNumber2 が見つからない...):
2013-07-26 17:39:48.760 Mbox Director-[デバッグ][15047:303] 起動中... { childDict = { MyNumber1 = 1; }; } 2013-07-26 17:39:48.760 Mbox Director-[デバッグ][15047:303] 終了中... { childDict = { MyNumber1 = 1; マイナンバー2 = 2; }; }
Lion の結果: 3 回目の実行 (通知、MyNumber2 が保存されました...): 2013-07-26 17:36:23.886 Mbox Director-[Debug][17013:120b] startup... {
childDict = { MyNumber1 = 1 ; マイナンバー2 = 2; }; } 2013-07-26 17:36:23.938 Mbox Director-[デバッグ][17013:120b] 終了中... { childDict = { MyNumber1 = 1; マイナンバー2 = 2; }; }
// This function makes a deep mutable copy. NSDictionary and NSArray mutableCopy does not create a DEEP mutableCopy.
// We accomplish a deep copy by first serializing the dictionary
// to a property list, and then unserializing it to a guaranteed deep copy.
// It requires that your array is serializable, of course.
// This method seems to be more bulletproof than some of the other implementations
// available on the web.
//
// Follows copy rule... you are responsible for releasing the returned object.
// Returns nil if not serializable!
id mutableCopyFromPlist( id plist )
{
NSError *error = nil;
@try
{
#ifdef MAC_OS_X_VERSION_10_6
NSData *binData = [NSPropertyListSerialization dataWithPropertyList:plist
format:NSPropertyListBinaryFormat_v1_0
options:0
error:&error];
NSString *errorString = [error localizedDescription];
#else
NSString *errorString = nil;
NSData *binData = [NSPropertyListSerialization dataFromPropertyList:plist
format:NSPropertyListBinaryFormat_v1_0
errorDescription:&errorString];
#endif
if (errorString || !binData )
{
DLogErr( @"error serializing property list %@", errorString );
}
else
{
#ifdef MAC_OS_X_VERSION_10_6
NSError *error = nil;
id deepCopy = [NSPropertyListSerialization
propertyListWithData:binData
options:NSPropertyListMutableContainersAndLeaves
format:NULL
error:&error];
errorString = [error localizedDescription];
#else
id deepCopy = [NSPropertyListSerialization
propertyListFromData:binData
mutabilityOption:NSPropertyListMutableContainersAndLeaves
format:NULL
errorDescription:&errorString];
#endif
[deepCopy retain]; // retain this so that we conform to the 'copy rule'... our function name contains the work 'Copy'
if (errorString)
{
DLogErr( @"error serializing property list %@", errorString );
}
else
{
return deepCopy;
}
}
}
@catch (NSException *exception )
{
DLogErr( @"error serializing property list %@", [error localizedDescription] );
}
return nil; // couldn't make a deep copy... probably not serializable
}
@implementation NSDictionary (VNSDictionaryCategory)
// This function makes a deep mutable copy. NSDictionary's mutableCopy does not create a DEEP mutableCopy.
// We accomplish a deep copy by first serializing the dictionary
// to a property list, and then unserializing it to a guaranteed deep copy.
// It requires that your dictionary is serializable, of course.
// This method seems to be more bulletproof than some of the other implementations
// available on the web.
//
// Follows copy rule... you are responsible for releasing the returned object.
// Returns nil if not serializable!
-(NSMutableDictionary *)mutableCopyDeepPropertyList
{
return mutableCopyFromPlist( self );
}
@end
#pragma mark -
@implementation NSArray (VNSArrayCategory)
// This function makes a deep mutable copy. NSDictionary's mutableCopy does not create a DEEP mutableCopy.
// We accomplish a deep copy by first serializing the dictionary
// to a property list, and then unserializing it to a guaranteed deep copy.
// It requires that your array is serializable, of course.
// This method seems to be more bulletproof than some of the other implementations
// available on the web.
//
// Follows copy rule... you are responsible for releasing the returned object.
// Returns nil if not serializable!
-(NSMutableArray *)mutableCopyDeepPropertyList
{
return mutableCopyFromPlist( self );
}
@end
使用法:
NSMutableDictionary *dict = [[NSUserDefaults standardUserDefaults] objectForKey:@"mydictionary"];
dict = [[dict mutableCopyDeepPropertyList] autorelease];