9

I added a Settings bundle to my app and in Xcode it appears in the root of my project tree view.

The Root.plist file looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>StringsTable</key>
    <string>Root</string>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>Type</key>
            <string>PSGroupSpecifier</string>
            <key>Title</key>
            <string>Service</string>
        </dict>
        <dict>
            <key>Type</key>
            <string>PSTextFieldSpecifier</string>
            <key>Title</key>
            <string>Hostname</string>
            <key>Key</key>
            <string>service_hostname</string>
   <!-- and so on -->

When I open the Settings app on iOS the entry appears at the bottom and I can display and edit my settings perfectly fine.

However I cannot retrieve these values from code. Here's my code:

static func loadSettings() {

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

    Settings.hostName = ud.stringForKey("service_hostname")
    // etc
}

I also tried ud.objectForKey and ud.valueForKey - both return nil as well.

After setting Settings.hostName the Xcode debugger reports it has a value of nil despite me setting an explicit value in the Settings app.

I saw this thread ( iPhone App : How to get default value from root.plist? ) where someone posted a chunk of Objective-C code that manually loads the Root.plist file directly into an NSMutableDictionary and calls NSUserDefaults.standardUserDefaults().registerDefaults but that seems like a hack (and I can't get it to work in Swift because the compiler says that stringByAppendingPathComponent doesn't exist anymore)

Why isn't NSUserDefaults picking up the settings from the Settings app?

Community
  • 1
  • 1
Dai
  • 141,631
  • 28
  • 261
  • 374

4 Answers4

13

Apparently the cause is that if my settings in the plist have defaults defined and the user has not explicitly set a value, then the value displayed in the Settings app will be the defaults from the plist file, however the NSUserDefaults API will still return nil.

Unfortunately this means that if the default value is meaningful (such as a default web-service address URI: "http://www.example.com") it must exist twice in my project: as a default in the plist and in my program code:

Root.plist:

  <dict>
      <key>Key</key>          <string>mySettingKey</string>
      <key>Title</key>        <string>Some address</string>
      <key>Type</key>         <string>PSTextFieldSpecifier</string>
      <key>DefaultValue</key> <string>http://www.example.com</string>
      <key>IsSecure</key>     <false />
      <key>KeyboardType</key> <string>Alphabet</string>
  </dict>

Program.swift:

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

var mySettingValue = ud.stringForKey("mySettingKey")
if mySettingValue == nil {
    mySettingValue = "http://www.example.com"
}

That's surprising.

Dai
  • 141,631
  • 28
  • 261
  • 374
  • I think I was going crazy. Searched for a solution for a long time! Why is this happening? Is there anyone who knows? – robertsan May 17 '16 at 06:31
  • @Dai this doesn't work for me, it gives nil value.. am I missing something? – mm24 Mar 23 '17 at 16:07
  • @mm24 "it gives a nil value" - that's what my posting is about and why the solution is to check for nil values and provide a default value in code. – Dai Mar 23 '17 at 17:06
  • @Dai thanks! The thing I struggle with is that I provided a default value in the Settings.bundle, so why do I need to provide a default value programmatically as well? – mm24 Mar 23 '17 at 20:47
  • @mm24 I don't know why we do, but we have to :) – Dai Mar 23 '17 at 21:45
  • Wow, this is insane. How come there's no way of accessing the values from the .plist file from code and initializing the USerDefaults with it? – tzippy Aug 28 '17 at 06:47
  • I just came across this solution that I will try out: https://stackoverflow.com/questions/510216/can-you-make-the-settings-in-settings-bundle-default-even-if-you-dont-open-the – tzippy Aug 28 '17 at 06:50
11

Default values can be taken from Settings.bundle and added to UserDefaults. Following function can be called in AppDelegate.swift from didFinishLaunchingWithOptions.

func setDefaultsFromSettingsBundle() {
    //Read PreferenceSpecifiers from Root.plist in Settings.Bundle
    if let settingsURL = Bundle.main.url(forResource: "Root", withExtension: "plist", subdirectory: "Settings.bundle"),
        let settingsPlist = NSDictionary(contentsOf: settingsURL),
        let preferences = settingsPlist["PreferenceSpecifiers"] as? [NSDictionary] {

        for prefSpecification in preferences {

            if let key = prefSpecification["Key"] as? String, let value = prefSpecification["DefaultValue"] {

                //If key doesn't exists in userDefaults then register it, else keep original value
                if UserDefaults.standard.value(forKey: key) == nil {

                    UserDefaults.standard.set(value, forKey: key)
                    NSLog("registerDefaultsFromSettingsBundle: Set following to UserDefaults - (key: \(key), value: \(value), type: \(type(of: value)))")
                }
            }
        }
    } else {
        NSLog("registerDefaultsFromSettingsBundle: Could not find Settings.bundle")
    }
}
Najdan Tomić
  • 2,071
  • 16
  • 25
  • 1
    There is a much better use here. Once you get `preferences`, use it with `UserDefaults register`. It's much better than looping through and explicitly setting each value. – rmaddy Oct 10 '17 at 21:11
  • @rmaddy Here I check if UserDefaults already contains specific key and I don't set in that case. Not sure how UserDefaults.register(:) will work in this case – Najdan Tomić Oct 11 '17 at 08:49
  • You can replace your entire `for` loop with the call to `register`. That's it. No need for the loop. No need to explicitly set any values. Just register the defaults with that one line and you are done. Much simpler. – rmaddy Oct 11 '17 at 14:40
  • @rmaddy I think that we don't understand each other here. If I replace `for` loop with check if key already exists in `UserDefaults` with `register`, all settings that are changed meanwhile and are in `UserDefaults` (same keys) will be replaced with default values from `Settings.bundle`. Keys may exist if application was already installed and running for some time, users may already changed something in the settings and it should stay like that. But you are correct that your solution is much simpler, and I would use your solution in case if app wasn't installed before (new app). – Najdan Tomić Oct 11 '17 at 17:07
  • 3
    No. The call to `register` doesn't change any values. That's not how it works. When you attempt to lookup a value in `UserDefaults`, if there is an explicitly set value then it is returned. If there is no explicitly set value, then the registered defaults is consulted. If there is a value there, then that default value is returned, otherwise `nil` is returned. Nothing about `register` has any effect on any existing values. – rmaddy Oct 11 '17 at 17:19
  • @rmaddy Okay didn't know that. Thank you a lot for explanation, I will test it later when I get time to be sure and will edit answer if everything works as expected. – Najdan Tomić Oct 11 '17 at 19:04
7

You should register your defaults so that it will sync up. source from here

// Swift 3
var appDefaults = Dictionary<String, AnyObject>()
appDefaults["mySettingKey"] = "http://www.example.com" // Default Value

UserDefaults.standard.register(appDefaults)
UserDefaults.standard.synchronize()

let mySettingValue = UserDefaults.standard.string(forKey: "mySettingKey")

Keep in mind that you should also use registerDefaults: when your app uses a Settings Bundle. Since you already specified default values inside the settings bundle’s plist, you may expect that your app picks these up automatically. However, that is not the case. The information contained in the settings bundle is only read by the iOS Settings.app and never by your app. In order to have your app use the same defaults as shown inside the Settings.app, you have to manually copy the user defaults keys and their default values into a separate plist file and register it with the defaults database as shown above.

Willjay
  • 6,381
  • 4
  • 33
  • 58
  • Interestingly enough, when I registered and synch'd my values, then immediately turned around and queried their values I got nothing ... had to do the old-style "setObject:forKey:" instead. – BonanzaDriver Sep 17 '16 at 17:12
  • 1
    Calling `synchronize` is pointless here. 1. Calling `register` doesn't actually change anything so there is nothing to synchronize. 2. Calling `synchronize` is never really needed any way. – rmaddy Oct 10 '17 at 21:05
0

You need to manually read the default values from Settings.bundle and add them to the UserDefaults registration domain, which can be done with UserDefaults.register(). By using the registration domain, the values are held in memory (volatile), so calling UserDefaults.synchronize() is not needed.

Swift 4 and 5

Here's a few lines of Swift that will get the job done:

// Register the default values from Settings.bundle
if let settingsURL = Bundle.main.url(forResource: "Root", withExtension: "plist", subdirectory: "Settings.bundle"),
    let settingsRootDict = NSDictionary(contentsOf: settingsURL),
    let prefSpecifiers = settingsRootDict["PreferenceSpecifiers"] as? [NSDictionary],
    let keysAndValues = prefSpecifiers.map({ d in (d["Key"], d["DefaultValue"]) }) as? [(String, Any)] {
        UserDefaults.standard.register(defaults: Dictionary(uniqueKeysWithValues: keysAndValues))
}
jrc
  • 20,354
  • 10
  • 69
  • 64