6
\$\begingroup\$

I've been struggling with registering a plist into NSUserDefaults and I've finally got it working, though I'm not sure that its written properly. I'm hoping someone here can shine some light on this process.

My intention with this code:

I want to be able to set all of my values in a plist and essentially use that to "seed" my NSUserDefaults. My end goal is to use this as a user settings menu.

My question:

What is the correct way to register a plist into the NSUserDefaults?

I followed the example from this this post and I received compiler errors. So I started experimenting. I could only get the code from the post to work if I added setObject(dict, forKey: "defaults") first.

Once added, it seemed to render the registerDefaults line pointless as the code runs with or without it.

The lines that I am questioning are these:

userDefaults.setObject(dict, forKey: "defaults") // without this my code doesn't work

userDefaults.registerDefaults(dict as! [NSObject : AnyObject])  // with or without this code works... do I need this?

I've placed the entirety of the code below to put above lines in context. This is all of the code so you can copy it right into Xcode and it will work.

ViewController.swift

class ViewController: UIViewController {

    let userDefaults = NSUserDefaults.standardUserDefaults()

    override func viewDidLoad() {
        super.viewDidLoad()

        let prefs = NSBundle.mainBundle().pathForResource("Settings", ofType: "plist")
        let dict = NSDictionary(contentsOfFile: prefs!)
        userDefaults.setObject(dict, forKey: "defaults")                // without this code doesn't work
        userDefaults.registerDefaults(dict as! [NSObject : AnyObject])  // with or without this code works... do I need this?
        userDefaults.synchronize()

        // this just toggles the aBool value
        if let defaults = userDefaults.valueForKey("aBool") as? NSNumber {

            if defaults as NSObject == 1 {
                println("inside the conditional: \(defaults)")
                userDefaults.setValue(0, forKey: "aBool")
                userDefaults.synchronize()
                println("Setting aBool to true")
            } else {
                userDefaults.setValue(1, forKey: "aBool")
                userDefaults.synchronize()
                println("setting aBool to false")
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()

    }
}

Settings.plist

enter image description here

\$\endgroup\$
5
\$\begingroup\$
override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
}

If all your method does is call up to the super class's implementation of the method, you can safely omit the method entirely. I understand this is boilerplate code that Xcode automatically inserts any time you create a new view controller, but at the end of the day, it's just clutter that should be cleaned up.


override func viewDidLoad() {
    super.viewDidLoad()

    let prefs = NSBundle.mainBundle().pathForResource("Settings", ofType: "plist")
    let dict = NSDictionary(contentsOfFile: prefs!)
    userDefaults.setObject(dict, forKey: "defaults")                // without this code doesn't work
    userDefaults.registerDefaults(dict as! [NSObject : AnyObject])  // with or without this code works... do I need this?
    userDefaults.synchronize()

    // this just toggles the aBool value
    if let defaults = userDefaults.valueForKey("aBool") as? NSNumber {
       if defaults as NSObject == 1 {
            println("inside the conditional: \(defaults)")
            userDefaults.setValue(0, forKey: "aBool")
            userDefaults.synchronize()
            println("Setting aBool to true")
        } else {
            userDefaults.setValue(1, forKey: "aBool")
            userDefaults.synchronize()
            println("setting aBool to false")
        }
    }
}

I don't want to say this is objectively too much code for viewDidLoad, but it's not appropriate for this to all go in viewDidLoad. This really doesn't belong in the viewDidLoad of any view controller, really. This is code we seem to only want to load once. This really belongs in our app delegate, in application(_:didFinishLaunchingWithOptions:).

Once we get our list loaded into NSUserDefaults, we just need to access the values through NSUserDefaults, which is a singleton we can easily access from anywhere.

As a final note about all of this, calling synchronize is generally going to be unnecessary. NSUserDefaults will synchronize itself when it finds a convenient time. Moreover, all synchronize does is synchronize what NSUserDefaults has stored in memory with what it has stored in long-term storage. The only time you can lose something that you put in NSUserDefaults is if your app crashes after you changed a value and before NSUserDefaults had a chance to synchronize. This is extraordinarily rare. And for emphasis, realize that I said this happens when your app crashes. If the user exits your app, NSUserDefaults will synchronize before the app is allowed to completely exit.

And when you access the values in NSUserDefaults, you'll get the value it was most recently set to (which isn't necessarily what is saved to the permanent storage at that moment).

Calling synchronize just forces the synchronization to happen at that very instant, which is entirely unnecessary.


And, for the record, our inner most conditional can be simplified:

if let defaults = userDefaults.valueForKey("aBool") as? NSNumber {
    userDefaults.setValue(!defaults.boolValue, forKey: "aBool")
}
\$\endgroup\$
  • \$\begingroup\$ Thank you so much for the answer! I'll give this a shot tonight when I get home! \$\endgroup\$ – Dan Beaulieu Apr 18 '15 at 23:07
  • 1
    \$\begingroup\$ are both the "setObject" & the "registerDefaults" necessary? \$\endgroup\$ – Dan Beaulieu Apr 19 '15 at 1:24
3
\$\begingroup\$

This line:

userDefaults.registerDefaults(dict as! [NSObject : AnyObject]) 

does not actually set the information on the NSUserDefaults storage file on disk. All it does is tell NSUserDefaults the default values to be used when a key doesn't yet exist on disk. So, for example, if NSUserDefaults has the following dictionary stored:

["key1": "value1"]

And you run this line:

userDefaults.registerDefaults(["key1": "defaultvalue1", "key2": "defaultvalue2"])

Then this is what you get when you ask NSUserDefaults for values:

userDefaults.valueForKey("key1") // -> "value1"
userDefaults.valueForKey("key2") // -> "defaultvalue2"

Please note that registerDefaults does not store the dictionary you give to disk (see docs: "The contents of the registration domain are not written to disk").

So, in your specific case, the second line is redundant because on the first line you're setting the values to disk. That means that you don't need to set default/fallback values for those keys (which is what you're doing on the second line), and even if you did, they'd never be read (assuming the keys are the same on both).

Normally you would call this method once in the initialization of your app with the default values for the keys. Then, as the user uses the app, your code begins writing actual values to disk according to the user's actions/needs/preferences (using setObject(_: forKey:)) which will override the defaults.

\$\endgroup\$
  • \$\begingroup\$ while both of these answers have been very revealing to me this answer has more directly answered my question. Thanks Alex! \$\endgroup\$ – Dan Beaulieu Apr 19 '15 at 13:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.