58

I have an iPhone application with a settings.bundle that handles various settings for my application. I can set default values in my root.plist file (using the DefaultValue property), but these only get used the first time the user opens the settings app. Is there any way to get these values written out when your application installs? I know I can just write code that checks for the first launch of my app and then write them out, but then they are in two different places.

Here is an entry from my root.plist as an example:

<dict>
    <key>Type</key>
    <string>PSToggleSwitchSpecifier</string>
    <key>Title</key>
    <string>Open To Top Location</string>
    <key>Key</key>
    <string>open_top_location</string>
    <key>DefaultValue</key>
    <string>YES</string>
    <key>TrueValue</key>
    <string>YES</string>
    <key>FalseValue</key>
    <string>NO</string>
</dict>

The end result should be that if I ask for 'open_to_top_location' I get a YES, instead of it not being there at all until the first time the user opens the Settings app.

Any ideas?

rustyshelf
  • 44,963
  • 37
  • 98
  • 104

8 Answers8

96

If I understood you correctly, you want to avoid having default values specified twice (once as "DefaultValue" keys in your Settings.bundle/Root.plist file, and once in your app initialization code) so you do not have to keep them in sync.

Since Settings.bundle is stored within the app bundle itself, you can just read the default values given there. I put together some sample code that looks at the Settings bundle and reads the default values for every key there. Note that this does not write out the default keys; if they don't exist, you'll need to read and register them at every launch (feel free to change this). I've only done some cursory tests, so make sure it works for you in all cases.

- (void)applicationDidFinishLaunching:(UIApplication *)application {    
    NSString *name = [[NSUserDefaults standardUserDefaults] stringForKey:@"name"];
    NSLog(@"name before is %@", name);

    // Note: this will not work for boolean values as noted by bpapa below.
    // If you use booleans, you should use objectForKey above and check for null
    if(!name) {
        [self registerDefaultsFromSettingsBundle];
        name = [[NSUserDefaults standardUserDefaults] stringForKey:@"name"];
    }
    NSLog(@"name after is %@", name);
}

- (void)registerDefaultsFromSettingsBundle {
    NSString *settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
    if(!settingsBundle) {
        NSLog(@"Could not find Settings.bundle");
        return;
    }

    NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]];
    NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"];

    NSMutableDictionary *defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]];
    for(NSDictionary *prefSpecification in preferences) {
        NSString *key = [prefSpecification objectForKey:@"Key"];
        if(key && [[prefSpecification allKeys] containsObject:@"DefaultValue"]) {
            [defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key];
        }
    }

    [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister];
    [defaultsToRegister release];
}
Samuel Clay
  • 1,252
  • 2
  • 13
  • 24
PCheese
  • 3,231
  • 28
  • 18
  • 3
    I was hoping for some magic bundle setting that I was missing like "WriteDefaultsOnInstall" but in the absence of that this will do, thanks :) – rustyshelf Feb 04 '09 at 07:18
  • That's some great sample code. The sample in AppPrefs is clunky compared to that. Thanks! – bbrown May 13 '09 at 04:43
  • 7
    One thing to note for this solution - it doesn't work if your Default is a boolean, since "if(!name)" returns NO if either the default is set to NO or if it doesn't exist. To get around this, I use objectForKey instead, since that will return 0, 1, or null. – bpapa Jun 12 '09 at 15:24
  • I was under the impression that the settings.bundle was read automatically after the App was launched. Looks like this isn't the case. The code above by PCheese solved the problem. Nice looking out! – Jordan Aug 20 '09 at 15:41
  • 5
    It is worth noting that `registerDefaultsFromSettingsBundle` does not need to be restricted to conditional execution: it can safely be run all the time, because `registerDefaults` never overwrites existing settings; it only provides default values for settings with no value. However, avoiding redundant execution saves a (tiny) bit of time. – tcovo Mar 28 '11 at 16:42
  • 1
    Note that calling `-setObject:` on defaultsToRegister with a `nil` key will throw an exception. I believe this will happen using the above code if `Key` is present but `DefaultValue` is not for a given preference. This is valid for, e.g., a Text Field preference (http://developer.apple.com/library/ios/#documentation/PreferenceSettings/Conceptual/SettingsApplicationSchemaReference/Articles/PSTextFieldSpecifier.html#//apple_ref/doc/uid/TP40007011-SW1). – Redwood Oct 31 '11 at 16:43
  • 3
    I have to note that this stuff does not work for localized applications: it opens `[settingsBundle stringByAppendingPathComponent:@"Root.plist"]]`, whereas for localized apps you need `localeid.lproj/Root.plist`. At second, `[defaultsToRegister setObject:...]` raises an exception for nil values. – 18446744073709551615 Nov 23 '11 at 10:34
  • This is great stuff. I added a tiny further check to make it more stable for incomplete settings: `NSString *key = [prefSpecification objectForKey:@"Key"]; NSString *value = [prefSpecification objectForKey:@"DefaultValue"]; if( key && value ) { [defaultsToRegister setObject:value forKey:key]; } ` – TomTom Jan 18 '12 at 19:27
  • @TomTom That won't work if the setting is a slider and it defaults to Off (since `value` evaluates quite correctly to `NO`). Also, does casting it to a string even work in that case? –  Jan 22 '12 at 22:37
  • I just changed this to a community wiki so you can merge in your improvements. – PCheese Jan 25 '12 at 07:25
12

Here is my code based on @PCheese's answer which adds support for keys without a default value and child panes.

- (void)registerDefaultsFromSettingsBundle {
    [[NSUserDefaults standardUserDefaults] registerDefaults:[self defaultsFromPlistNamed:@"Root"]];
}

- (NSDictionary *)defaultsFromPlistNamed:(NSString *)plistName {
    NSString *settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
    NSAssert(settingsBundle, @"Could not find Settings.bundle while loading defaults.");

    NSString *plistFullName = [NSString stringWithFormat:@"%@.plist", plistName];

    NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:plistFullName]];
    NSAssert1(settings, @"Could not load plist '%@' while loading defaults.", plistFullName);

    NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"];
    NSAssert1(preferences, @"Could not find preferences entry in plist '%@' while loading defaults.", plistFullName);

    NSMutableDictionary *defaults = [NSMutableDictionary dictionary];
    for(NSDictionary *prefSpecification in preferences) {
        NSString *key = [prefSpecification objectForKey:@"Key"];
        id value = [prefSpecification objectForKey:@"DefaultValue"];
        if(key && value) {
            [defaults setObject:value forKey:key];
        } 

        NSString *type = [prefSpecification objectForKey:@"Type"];
        if ([type isEqualToString:@"PSChildPaneSpecifier"]) {
            NSString *file = [prefSpecification objectForKey:@"File"];
            NSAssert1(file, @"Unable to get child plist name from plist '%@'", plistFullName);
            [defaults addEntriesFromDictionary:[self defaultsFromPlistNamed:file]];
        }        
    }

    return defaults;
}
Community
  • 1
  • 1
Redwood
  • 66,744
  • 41
  • 126
  • 187
10

Here is the Swift version: call it from:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.
    self.registerDefaultsFromSettingsBundle()

    return true
}

converted function:

func registerDefaultsFromSettingsBundle(){
    //NSLog("Registering default values from Settings.bundle");
    let defs: NSUserDefaults = NSUserDefaults.standardUserDefaults()
    defs.synchronize()

    var settingsBundle: NSString = NSBundle.mainBundle().pathForResource("Settings", ofType: "bundle")!    
    if(settingsBundle.containsString("")){
        NSLog("Could not find Settings.bundle");
        return;
    }
    var settings: NSDictionary = NSDictionary(contentsOfFile: settingsBundle.stringByAppendingPathComponent("Root.plist"))!
    var preferences: NSArray = settings.objectForKey("PreferenceSpecifiers") as NSArray
    var defaultsToRegister: NSMutableDictionary = NSMutableDictionary(capacity: preferences.count)

    for prefSpecification in preferences {
        if (prefSpecification.objectForKey("Key") != nil) {
            let key: NSString = prefSpecification.objectForKey("Key")! as NSString
            if !key.containsString("") {
                let currentObject: AnyObject? = defs.objectForKey(key)
                if currentObject == nil {
                    // not readable: set value from Settings.bundle
                    let objectToSet: AnyObject? = prefSpecification.objectForKey("DefaultValue")
                    defaultsToRegister.setObject(objectToSet!, forKey: key)
                    NSLog("Setting object \(objectToSet) for key \(key)")
                }else{
                    //already readable: don't touch
                    //NSLog("Key \(key) is readable (value: \(currentObject)), nothing written to defaults.");
                }
            }
        }
    }
    defs.registerDefaults(defaultsToRegister)
    defs.synchronize()
}
yanko
  • 121
  • 1
  • 5
  • "prefSpecification" might not have a key "DefaultValue" as a result objectToSet will be NULL. Solution: ----if let objectToSet: AnyObject = prefSpecification.objectForKey("DefaultValue") as AnyObject?--- – Yan Mar 22 '15 at 12:48
  • This code is not working in Swift 2... `Cannot convert value of type 'NSMutableDictionary' to expected argument type '[String : AnyObject]'` How to fix? –  Aug 27 '15 at 07:24
  • 1
    doing this for now...: func registerDefaultsFromSettingsBundle(){ //NSLog("Registering default values from Settings.bundle"); let defs: NSUserDefaults = NSUserDefaults.standardUserDefaults() defs.synchronize() let settingsBundle: NSString = NSBundle.mainBundle().pathForResource("Settings", ofType: "bundle")! if(settingsBundle.containsString("")){ NSLog("Could not find Settings.bundle"); return; } – yanko Nov 06 '15 at 19:19
  • let settings: NSDictionary = NSDictionary(contentsOfFile: settingsBundle.stringByAppendingPathComponent("Root.plist"))! let preferences: NSArray = settings.objectForKey("PreferenceSpecifiers") as! NSArray let defaultsToRegister: NSMutableDictionary = NSMutableDictionary(capacity: preferences.count) for prefSpecification in preferences { – yanko Nov 06 '15 at 19:21
  • if (prefSpecification.objectForKey("Key") != nil) { let key: NSString = prefSpecification.objectForKey("Key")! as! NSString if !key.containsString("") { let currentObject: AnyObject? = defs.objectForKey(key as String) if currentObject == nil { // not readable: set value from Settings.bundle – yanko Nov 06 '15 at 19:21
  • let objectToSet: AnyObject? = prefSpecification.objectForKey("DefaultValue") defaultsToRegister.setObject(objectToSet!, forKey: key) NSLog("Setting object \(objectToSet) for key \(key)") }else{ //already readable: don't touch NSLog("Key \(key) is readable (value: \(currentObject)), nothing written to defaults."); } } – yanko Nov 06 '15 at 19:21
  • } } defs.registerDefaults((defaultsToRegister as NSDictionary as? [String : AnyObject])!) defs.synchronize() } – yanko Nov 06 '15 at 19:21
6

Swift 3 version

    func registerDefaultsFromSettingsBundle(){
    guard let settingsBundle = Bundle.main.path(forResource: "Settings", ofType: "bundle") else {
        print("Could not locate Settings.bundle")
        return
    }

    guard let settings = NSDictionary(contentsOfFile: settingsBundle+"/Root.plist") else {
        print("Could not read Root.plist")
        return
    }

    let preferences = settings["PreferenceSpecifiers"] as! NSArray
    var defaultsToRegister = [String: AnyObject]()
    for prefSpecification in preferences {
        if let post = prefSpecification as? [String: AnyObject] {
            guard let key = post["Key"] as? String,
                let defaultValue = post["DefaultValue"] else {
                    continue
            }
            defaultsToRegister[key] = defaultValue
        }
    }
    UserDefaults.standard.register(defaults: defaultsToRegister)
}
Anders Cedronius
  • 2,036
  • 1
  • 23
  • 29
3

A Swift 2 compatible version

func registerDefaultsFromSettingsBundle(){

    let defaults = NSUserDefaults.standardUserDefaults()
    defaults.synchronize()

    let settingsBundle: NSString = NSBundle.mainBundle().pathForResource("Settings", ofType: "bundle")!
    if(settingsBundle.containsString("")){
        NSLog("Could not find Settings.bundle");
        return;
    }
    let settings = NSDictionary(contentsOfFile: settingsBundle.stringByAppendingPathComponent("Root.plist"))!
    let preferences = settings.objectForKey("PreferenceSpecifiers") as! NSArray;
    var defaultsToRegister = [String: AnyObject](minimumCapacity: preferences.count);

    for prefSpecification in preferences {
        if (prefSpecification.objectForKey("Key") != nil) {
            let key = prefSpecification.objectForKey("Key")! as! String
            if !key.containsString("") {
                let currentObject = defaults.objectForKey(key)
                if currentObject == nil {
                    // not readable: set value from Settings.bundle
                    let objectToSet = prefSpecification.objectForKey("DefaultValue")
                    defaultsToRegister[key] = objectToSet!
                    NSLog("Setting object \(objectToSet) for key \(key)")
                }
            }
        }
    }
    defaults.registerDefaults(defaultsToRegister)
    defaults.synchronize()
}
JTango18
  • 633
  • 6
  • 11
0

A much cleaner swift 2.2 version, requires a quick extension on string to restore stringByAppendingPathComponent:

extension String {
    func stringByAppendingPathComponent(path: String) -> String {
        let nsSt = self as NSString
        return nsSt.stringByAppendingPathComponent(path)
    }
}

func registerDefaultsFromSettingsBundle() {
    guard let settingsBundle = NSBundle.mainBundle().pathForResource("Settings", ofType: "bundle") else {
        log.debug("Could not find Settings.bundle")
        return
    }

    let settings = NSDictionary(contentsOfFile: settingsBundle.stringByAppendingPathComponent("Root.plist"))!

    let preferences = settings["PreferenceSpecifiers"] as! NSArray

    var defaultsToRegister = [String: AnyObject]()

    for prefSpecification in preferences {
        guard let key = prefSpecification["Key"] as? String,
        let defaultValue = prefSpecification["DefaultValue"] else {
            continue
        }

        defaultsToRegister[key] = defaultValue
    }
    NSUserDefaults.standardUserDefaults().registerDefaults(defaultsToRegister)
}
JuJoDi
  • 14,627
  • 23
  • 80
  • 126
0

One more version of the same theme. I kept Lawrence Johnston's support for child panes and added the i18n/l10n support.

// This code is folklore, first created by an unknown person and copied, pasted
// and published by many different programmers, each (hopefully) of whom added
// some improvemrnts. (c) the People of the Earth
- (NSDictionary *)defaultsFromPlistNamed:(NSString *)plistName {
    NSString *settingsBundlePath = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
    if (!settingsBundlePath) {
        NSAssert(settingsBundlePath, @"Could not find Settings.bundle while loading defaults.");
        return nil;
    }

    NSBundle *settingsBundle = [NSBundle bundleWithPath:settingsBundlePath];
    if (!settingsBundlePath) {
        NSAssert(settingsBundle, @"Could not load Settings.bundle while loading defaults.");
        return nil;
    }

    NSString *plistFullName = [settingsBundle pathForResource:plistName ofType:@"plist"];
    if (!plistName) {
        NSAssert1(settings, @"Could not find plist '%@' while loading defaults.", plistFullName);
        return nil;
    }

    NSDictionary *settings_dic = [NSDictionary dictionaryWithContentsOfFile:plistFullName];
    if (!settings_dic) {
        NSAssert1(settings_dic, @"Could not load plist '%@' while loading defaults.", plistFullName);
        return nil;
    }

    NSArray *preferences = [settings_dic objectForKey:@"PreferenceSpecifiers"];
    NSAssert1(preferences, @"Could not find preferences entry in plist '%@' while loading defaults.", plistFullName);

    NSMutableDictionary *defaults = [NSMutableDictionary dictionary];
    for(NSDictionary *prefSpecification in preferences) {
        NSString *key = [prefSpecification objectForKey:@"Key"];
        if (key) {
            id value = [prefSpecification objectForKey:@"DefaultValue"];
            if(value) {
                [defaults setObject:value forKey:key];
                NSLog(@"setting %@ = %@",key,value);
            } 
        }

        NSString *type = [prefSpecification objectForKey:@"Type"];
        if ([type isEqualToString:@"PSChildPaneSpecifier"]) {
            NSString *file = [prefSpecification objectForKey:@"File"];
            NSAssert1(file, @"Unable to get child plist name from plist '%@'", plistFullName);
            if (file) {
                [defaults addEntriesFromDictionary:[self defaultsFromPlistNamed:file]];
            }
        }        
    }

    return defaults;
}

- (void)registerDefaultsFromSettingsBundle {
    [[NSUserDefaults standardUserDefaults] registerDefaults:[self defaultsFromPlistNamed:@"Root"]];
}

Call [self registerDefaultsFromSettingsBundle]; from - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

if(x) {NSAssert(x);return nil;} looks stupid, but I feel lazy to do something about it.

18446744073709551615
  • 16,368
  • 4
  • 94
  • 127
0

A different approach: code generation

The following generates an Objective-C file with a single function that registers the defaults for Root.plist.

xsltproc settings.xslt Settings.bundle/Root.plist > registerDefaults.m

In can be run automatically using a "Run Script" build phase in XCode. The phase should be placed before "Compile Sources". (xsltproc comes with OS X.)

"Run Script" screenshot

This is somewhat basic and doesn't handle nested files, but maybe somebody has a use for it.

settings.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes" indent="no" />

  <xsl:template match="dict">
    <xsl:choose>
      <xsl:when test="key[.='DefaultValue']/following-sibling::*[position()=1 and self::true]">
        @"YES",
      </xsl:when>
      <xsl:when test="key[.='DefaultValue']/following-sibling::*[position()=1 and self::false]">
        @"NO",
      </xsl:when>
      <xsl:otherwise>
        @"<xsl:value-of select="key[.='DefaultValue']/following-sibling::*[1]"/>",
      </xsl:otherwise>
    </xsl:choose>
    @"<xsl:value-of select="key[.='Key']/following-sibling::*[1]"/>",
  </xsl:template>

  <xsl:template match="/">
    void registerDefaults() {

    NSDictionary *defaults =
    [NSDictionary dictionaryWithObjectsAndKeys:
    <xsl:apply-templates select="descendant::key[.='DefaultValue']/.."/>
    nil];

    [[NSUserDefaults standardUserDefaults] registerDefaults: defaults];
    }
  </xsl:template>

</xsl:stylesheet>

The is based on the work of Benjamin Ragheb.

nschum
  • 15,322
  • 5
  • 58
  • 56