为什么NSUserDefaults无法在iOS中保存NSMutableDictionary?

145
我想在NSUserDefaults中保存一个NSMutableDictionary对象。 NSMutableDictionary中的键类型为NSString,值类型为包含实现NSCoding的对象列表的NSArray。根据文档,NSStringNSArray都符合NSCoding
我遇到了这个错误:

[NSUserDefaults setObject: forKey:]: Attempt to insert non-property value.... of class NSCFDictionary.


(注意:保留原有html标签)
6个回答

230

我找到了一个替代方案,即在保存之前,使用 NSKeyedArchiver 对根对象 (NSArray 对象) 进行编码,以生成 NSData 数据。然后使用 UserDefaults 保存该 NSData 数据。

当需要使用数据时,读取出 NSData 数据,并使用 NSKeyedUnarchiverNSData 转换回对象。

这有点麻烦,因为每次都需要进行 NSData 的编码和解码,但它能够正常工作。

以下是一个示例:

保存:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *arr = ... ; // set value
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:arr];
[defaults setObject:data forKey:@"theKey"];
[defaults synchronize];

加载:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults objectForKey:@"theKey"];
NSArray *arr = [NSKeyedUnarchiver unarchiveObjectWithData:data];

数组中的元素实现了

@interface CommentItem : NSObject<NSCoding> {
    NSString *value;
}

然后在实现CommentItem时,提供了两种方法:

-(void)encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeObject:value forKey:@"Value"];
}

-(id)initWithCoder:(NSCoder *)decoder
{
    self.value = [decoder decodeObjectForKey:@"Value"];
    return self;
}

有更好的解决方案吗?

谢谢大家。


4
你有可以分享的代码示例吗?我一直在尝试在帖子(537044)中完成这个,但是没有成功。 - Anthony Main
这对我来说很有效。我正在尝试对一个包含一些非字符串键的NSDictionary对象的NSArray进行编码。简单地使用NSKeyedArchiver和Unarchiver处理NSArray,就可以消除“尝试插入非属性值”的错误。 - John Wright
我何时需要释放已加载的对象?因为没有调用alloc,所以我会认为它是自动释放的? - Marc
7
我们可能需要添加 [defaults synchronize] 来保存。 - thanhbinh84
这里发布的解决方案可能会有一段时间的效果,但当您决定删除或更改类时,您会发现自己陷入了混乱。更好的解决方案是让您的对象使用NSNumbers、Strings等将其状态写入字典,然后存储该字典。具备未来性。 - Tom Andersen

105
如果您在用户默认设置中保存对象,则必须将所有对象(递归地一直到底部)都作为属性列表对象。符合NSCoding并不意味着什么--NSUserDefaults不会自动将它们编码为NSData,您必须自己做。如果您的“实现NSCoding的对象列表”是指非属性列表对象的对象,则在保存到用户默认设置之前,您需要对它们进行处理。
值得一提的是,属性列表类包括NSDictionary、NSArray、NSString、NSDate、NSData和NSNumber。您可以编写可变子类(如NSMutableDictionary)到用户首选项,但读取出来的对象始终是不可变的。

61
我还应该提到,如果键是NSStrings,那么NSDictionary只是一个属性列表对象。通常情况下,您可以使用其他对象作为字典键,但在需要属性列表的情况下,键必须是字符串。 - Tom Harrington
我遇到了同样的问题,当我想将NSURL作为对象存储在NSDictionary中并将其存储在NSUserDefaults中时。我不得不将它转换为NSString,然后它就可以工作了。 - NicTesla
1
太好了!在我的情况下,我尝试将NSNumber作为用户默认设置中NSDictionary的键。我必须将其更改为NSString。 - Joey
1
这种愚蠢的行为导致了经验不足的程序员将整个数据模型编写为字典,而不是制作适当的数据对象。苹果公司有多难以编写利用 obj-c 具有内省功能并为我们处理装箱和拆箱的基础类呢? - ArtOfWarfare

14

您的字典 NSString 中的所有键都在吗? 我认为必须这样才能将字典保存到属性列表中。


1
键是NSString,但值是NSArray,其元素是实现NSCoding的自定义类。似乎该解决方案不起作用,因为UserDefaults仅支持6种类型的类,而不考虑NSCoding。 - BlueDolphin

8

最简单的答案:

NSDictionary只是一个属性列表对象,如果键是NSStrings。因此,使用stringWithFormat"Key"存储为NSString


解决方案:

NSString *key = [NSString stringWithFormat:@"%@",[dictionary valueForKey:@"Key"]];

优点:

  1. 它将添加字符串值。
  2. 当您的变量值为NULL时,它将添加空值。

2
我遇到了类似的问题:我的键是NSNumbers,这在plist字典的键中显然是不允许的。 - Mark Pauley

5

您是否考虑过实现NSCoding协议?这将允许您使用在NSCoding中实现的两种简单方法在iPhone上进行编码和解码。首先,您需要将NSCoding添加到您的类中。

以下是一个示例:

这是.h文件中的内容:

@interface GameContent : NSObject <NSCoding>

那么你需要实现 NSCoding 协议的两个方法。

    - (id) initWithCoder: (NSCoder *)coder
    {
        if (self = [super init])
        {
               [self setFoundHotSpots:[coder decodeObjectForKey:@"foundHotSpots"]];
        }
        return self;
    }

    - (void) encodeWithCoder: (NSCoder *)coder
    {
           [coder encodeObject:foundHotSpots forKey:@"foundHotSpots"];
    }

查看有关NSCoder的文档以获取更多信息。在我的项目中,当iPhone应用程序关闭并且需要将其状态保存并在其恢复时非常方便。

关键是将协议添加到接口中,然后实现NSCoding的两个方法。

希望这可以帮助你!


0

没有更好的解决方案。另一个选择是将编码对象直接保存到磁盘上,但这也是在做同样的事情。它们最终都会得到NSData,当您需要时进行解码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接