Use Shared NSUserDefaults for XPC and Interface Builder Nibs

The default Interface Builder approach of using a "Shared User Defaults Controller" object breaks down if your app preferences are not stored in the standard place.

Both my apps The Archive and the Word Counter for Mac are comprised of multiple executable applications. For The Archive, it's the main app and the Quick Entry popup window. They share some settings, like which theme and font size is used. To share these settings, I rely on NSUserDefault (or just UserDefaults in Swift now). I cannot use the UserDefaults.standard, though, because that is tied to the currently running app's bundle ID. In the case of the main app, it's the ID of the main app; but for the Quick Entry helper – or any helper app –, it's the helper's bundle ID. This way, the defaults dictionaries are not shared.

I solve it this way:

  1. Use a custom App Group for the related apps (main app + helper app)
  2. Use UserDefaults(suiteName: "the.app.group.name") in all apps
  3. Replace Interface Builder's "Shared User Defaults Controller" with a custom one

App Group

To set up an app group,

  • go to the project settings in Xcode,
  • select the "Capabilities" tab,
  • enable "App Groups"
  • add a new entry there for your app's bundle ID of the format $(TeamIdentifierPrefix)com.example.app-bundle-id.prefs (I like the .prefs suffix, but it's optional)

Do this for all affected app targets in your project.

Custom UserDefaults instance

The simple version is to just initialize a new UserDefaults instance with the written-out version of the App Group you just created:

UserDefaults(suiteName: "XXXYYYZZZA.com.example.app-bundle-id.prefs")

But I in fact use multiple app groups, using the .prefs and .prefs-dev and .prefs-dev-testing suffixes. This way I can experiment with my Xcode DEBUG builds without affecting the real app on my machine that I rely on for a lot of my work!

#if DEBUG
fileprivate var sharedUserDefaults: UserDefaults 
    = UserDefaults(suiteName: "XXXYYYZZZA.com.example.app-bundle-id.prefs-dev")!
fileprivate var sharedUserDefaultsForTesting: UserDefaults 
    = UserDefaults(suiteName: "XXXYYYZZZA.com.example.app-bundle-id.prefs-dev-testing")!
// Testing seam; During testing, this is switched to another instance.
fileprivate var usedUserDefaults: UserDefaults 
    = sharedUserDefaults
#else
fileprivate var sharedUserDefaults: UserDefaults 
    = UserDefaults(suiteName: "XXXYYYZZZA.com.example.app-bundle-id.prefs")!
#endif

extension UserDefaults {
    #if DEBUG
    public static var shared: UserDefaults {
        return usedUserDefaults
    }

    enum Mode {
        case app, test
    }

    internal static func changeUsedUserDefaults(to mode: Mode) {
        usedUserDefaults = {
            switch mode {
            case .app:
                return sharedUserDefaults

            case .test:
                return sharedUserDefaultsForTesting
            }
        }()
    }
    #else
    public static var shared: UserDefaults {
        return sharedUserDefaults
    }
    #endif
}

That's probably doing more than you did expect! It boils down to exposing a UserDefaults.shared static property that I can use anywhere in the apps.

And during tests, I can replace the regular development preferences (which I often want to keep unchanged between manual testing runs) with another instance entirely that can be wiped and filled with garbage and whatnot. Then I call UserDefaults.changeUsedUserDefaults(to: .testing) in my XCTestCase.setUp implementation.

So this already works for programmatic changes.

How do you employ the result when designing preference panes in Interface Builder's Nibs, though?

Replacement for the regular NSUserDefaultsController

Interface Builder's "Shared User Defaults Controller" is of the "object" type and references an instance of NSUserDefaultsController. These are handy because they are Key-Value-Coding compliant. You can bind label texts and checkbox statuses to them easily.

But they are not configurable to point to the UserDefaults suite.

Instead of subclassing NSUserDefaultsController to provide such @IBDesignable capabilities, I reroute the KVO/KVC targets.

Take my base PreferenceViewController class, for example:

class PreferencesViewController: NSViewController {

    /// Controller for `UserDefaults.shared` so that KVO of view components
    /// doesn't use the (wrong) standard user defaults.
    @objc dynamic lazy var userDefaultsController: NSUserDefaultsController 
        = NSUserDefaultsController(defaults: self.defaults, initialValues: nil)
    var defaults: UserDefaults { 
        return UserDefaults.shared 
    }
}

My preference view controller subclass this type. In their Nib files, I set up a label text to display the last time Sparkle updater checked for an update like this:

  • Create label (NSTextField)
  • Below "Value With Pattern", bind the label's "Display Pattern Value1" to "File's Owner" (that's the view controller)
  • Set "Model Key Path" to self.userDefaultsController.values.SULastCheckTime
  • Set "Display Pattern" to Last Update: %{value1}@ (if you didn't use "Display Pattern Value1", you need to use another number here, e.g. value2)

And now I have the convenience of auto-updating labels and other UI items thanks to KVO/KVC plus a shared UserDefaults suite among my helper apps and the main app.

Your preference files will actually be stored in this location, by the way: ~/Library/Group\ Containers/XXXYYYZZZA.com.example.app-bundle-id.prefs/Library/Preferences/XXXYYYZZZA.com.example.app-bundle-id.prefs.plist.

Browse the blog archive