Articles

NSUserDefaults与Settings.bundle的数据同步

NSUserDefaults

在单独使用NSUserDefaults的时候,它的功能很明确。我们可以利用- objectForKey:和- setObject:forKey:来获取和保存用户数据。

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
id object = [defaults objectForKey:@"MyKey"];

Content: 用户数据一般保存在$HOME/Library/Preferences/.plist中,当用户从未保存过数据时,.plist文件不存在。在软件运行起来后,我们可以通过– registerDefaults:注册默认的用户数据。默认的用户数据写死在代码里,临时保存在内存中,并不会写入.plist文件。

Settings.bundle

用户的数据毕竟是需要用户来配置的,我们可以开发自己的配置界面,将配置界面集成到应用软件中,也可以将配置界面集成到iOS系统的配置界面中。按照苹果的官方指南Preferences and Settings Programming Guide,需要用户经常修改的参数应该集成到应用软件中,不太需要经常修改的数据可以放到系统的配置界面中。

对于我们自己开发的配置界面,我们可以在配置界面的Controller里同步配置界面与NSUserDefaults中的数据。对于放到iOS系统中的配置,系统会根据Settings.bundle/Root.plist及几个相关配置文件帮我们自动生成配置界面。用户在通过iOS系统配置我们的软件时,我们的软件也许还没有运行起来,所以系统不可能获取到我们代码中通过– registerDefaults:注册的默认的用户数据。所以,我们需要在Root.plist中再次指定每一个设置的默认值。

问题与解决办法

这样问题就来了,默认值在代码和Root.plist被重复设置,重复的数据总是不好的。苹果官方的AppPrefs里面提供了一个实现方法,我们可以遍历Root.plist中的默认参数,然后将其注册到NSUserDefaults中,这个示例代码的早期版本如下。

- (void)setupByPreferences
{
    NSString *testValue = [[NSUserDefaults standardUserDefaults] stringForKey:kFirstNameKey];
    if (testValue == nil)
    {
        // no default values have been set, create them here based on what's in our Settings bundle info
        //
        NSString *pathStr = [[NSBundle mainBundle] bundlePath];
        NSString *settingsBundlePath = [pathStr stringByAppendingPathComponent:@"Settings.bundle"];
        NSString *finalPath = [settingsBundlePath stringByAppendingPathComponent:@"Root.plist"];

        NSDictionary *settingsDict = [NSDictionary dictionaryWithContentsOfFile:finalPath];
        NSArray *prefSpecifierArray = [settingsDict objectForKey:@"PreferenceSpecifiers"];

        NSString *firstNameDefault = nil;
        NSString *lastNameDefault = nil;
        NSNumber *nameColorDefault = nil;
        NSNumber *backgroundColorDefault = nil;

        NSDictionary *prefItem;
        for (prefItem in prefSpecifierArray)
        {
            NSString *keyValueStr = [prefItem objectForKey:@"Key"];
            id defaultValue = [prefItem objectForKey:@"DefaultValue"];

            if ([keyValueStr isEqualToString:kFirstNameKey])
            {
                firstNameDefault = defaultValue;
            }
            else if ([keyValueStr isEqualToString:kLastNameKey])
            {
                lastNameDefault = defaultValue;
            }
            else if ([keyValueStr isEqualToString:kNameColorKey])
            {
                nameColorDefault = defaultValue;
            }
            else if ([keyValueStr isEqualToString:kBackgroundColorKey])
            {
                backgroundColorDefault = defaultValue;
            }
        }

        // since no default values have been set (i.e. no preferences file created), create it here
        NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
                                     firstNameDefault, kFirstNameKey,
                                     lastNameDefault, kLastNameKey,
                                     nameColorDefault, kNameColorKey,
                                     backgroundColorDefault, kBackgroundColorKey,
                                     nil];

        [[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }

    // we're ready to go, so lastly set the key preference values
    firstName = [[NSUserDefaults standardUserDefaults] stringForKey:kFirstNameKey];
    lastName = [[NSUserDefaults standardUserDefaults] stringForKey:kLastNameKey];
    textColor = [[NSUserDefaults standardUserDefaults] integerForKey:kNameColorKey];
    backgroundColor = [[NSUserDefaults standardUserDefaults] integerForKey:kBackgroundColorKey];
}

但是通过判断testValue是否为nil的方法不太可靠,因为iOS系统的配置界面仅会保存用户修改过的数据。很可能testValue不为空,但其它数据是空的。所以在这篇文章userDefaults: automatically register defaults from Settings.bundle中,作者给出了另外一个解决办法。他通过遍历Root.plist,查看其中每一个默认值是否已经可以通过NSUserDefaults获取到,获取不到的就把默认值保存下来,然后注册。这个办法的确避免了判断testValue是否为nil,但这个办法有点多余了。实际上根本不需要判断默认值是否已经可以通过NSUserDefaults获取到,全部取出来注册就是了。NSUserDefaults如果发现.plist中保存了用户配置,它会自动忽略通过– registerDefaults:注册的默认值。

苹果在最新版本的AppPrefs示例代码中,也对上述代码加以改进。

- (void)populateRegistrationDomain
{
    NSURL *settingsBundleURL = [[NSBundle mainBundle] URLForResource:@"Settings" withExtension:@"bundle"];

    // loadDefaults:fromSettingsPage:inSettingsBundleAtURL: expects its caller
    // to pass it an initialized NSMutableDictionary.
    NSMutableDictionary *appDefaults = [NSMutableDictionary dictionary];

    // Invoke loadDefaults:fromSettingsPage:inSettingsBundleAtURL: on the property
    // list file for the root settings page (always named Root.plist).
    [self loadDefaults:appDefaults fromSettingsPage:@"Root.plist" inSettingsBundleAtURL:settingsBundleURL];

    // appDefaults is now populated with the preferences and their default values.
    // Add these to the registration domain.
    [[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

新代码更加简洁有效,通过调用- loadDefaults:fromSettingsPage:inSettingsBundleAtURL:一次性获取全部默认值,然后通过- registerDefaults:注册就是了。