Make App Pie

Training for Developers and Artists

Using Settings Bundles with Swift

Have you ever wondered how to put  user defined settings for your app into the settings app?  Xcode can create a special property list called a settings bundle which can append the  NSUserDefaults with more entries from Settings App.  You’ll find out in this lesson how to set up a settings bundle in your app for using the Settings App using XML. In the next lesson we’ll dive deep into the settings bundle using the property list editor.

I’ll assume you know how to use property lists and NSUserDefaults in this lesson. If you are not familiar with them you might want to check out my recent posts on Property Lists and NSUserDefaults

Create the  Settings Bundle

Start a new Single view application  project in Xcode called SettingsBundleDemo using Swift and a Universal device.

Right-click on the SettingsBundleDemo group folder and select New file. Select the  Resources  category in the template window. Select Settings Bundle and click Next.

2016-03-11_05-57-54

You should have name of settings. Keep the setting bundle in the SettingsBundleDemo Group  in the save window. Click Create.

The system will open up the settings bundle. which looks like a building block. In the Navigator open up the settings bundle by clicking on the arrow.

2016-03-11_06-00-28

Click on the Root.plist file. You’ll see a property list  like this:

2016-03-13_12-33-22

If closed, Open the Preference Items array. This is populated with demo data. Unlike other property lists,  this one does not directly store the value as a dictionary of values, but an array of dictionaries of controls. You pick the control you want in the Settings app by listing it in the property list. When we attach the settings bundle to the standard defaults of NSUserDefaults, the system will create key values in the NSUserDefaults.

The following table lists the type of controls that you can use:

2016-03-13_15-54-53

Each type of control has several attributes.   If not already open, Open the Group Control, the Item 0 entry,  in Root.plist.

2016-03-13_12-38-07

Groups are control used to group other controls together. They have only a title and a Type. Open the Item 3 (Slider).

2016-03-13_12-39-56

The slider has more required controls, the ones listed here.  Many controls will have a  poorly named attribute Default Value. This is the value shown in  the settings app, not a true value of the default. It will show the values after the first use, but it is for display purposes not value purposes.

Each control type has its own attributes, as described in the chart above.  Here’s a list of all the possible attributes for the property list.

2016-03-14_08-11-55

Making Your Own Settings

Delete the current entries. Property lists do not delete well in Root.plist.  The simplest way is to cut them out. Select Item 0 (Group). Right click on group, and select Cut in the menu. Then do the same for the Item 3( Slider)  and the rest of the controls. You should have a clean property list like this.

2016-03-13_12-43-40

Select Preference Items.  Make sure it is open. The arrow should be pointing down.  Press the Add (+) button on the entry. A new entry appears.

2016-03-13_12-48-19

There will also be a menu that opens. If you happen to click somewhere and lose the menu, click the down chevron(2016-03-13_12-50-18)  in the entry. You get a list of controls.

2016-03-13_12-51-21

Select Toggle Switch. This will give us a Bool value.  Open up the toggle switch control and you have the following attributes:

2016-03-13_12-53-55

Click the value for each blank attribute, and set Title to  Room for cream.  Set Identifier to coffee_cream.  The identifier is the key for the NSUserDefaults entry.  Set the Default Value to YES. Your attributes should look like this:

2016-03-13_12-57-07

Close the attributes for the switch, and select Item 0. Right click and select Add Row from the menu. Although it is the default, in the type menu, select Text Field.   Open up the Item 1(Text Field – ) to see the attributes.

2016-03-13_13-01-53

 

Set the Title to Beverage of choice.  Set the Identifier to coffee_type. Right click on Identifier and select Add Row. A new row appears asking for an appropriate attribute:

2016-03-13_13-08-55

Select the default Default Value. Set the Default Value to Coffee.

2016-03-13_13-08-56

Close the attributes for the Text menu. Select the  Preference items entry, right click  and select Add Row.  The new row pushed the other rows down becoming Item 0. Select Multi-Value. Multi-value uses two arrays to make a selection table. Open up the attributes of the multi-value.  Set the title to Size. Set the Default Value to 0. Set the Identifier to coffee_size.

2016-03-13_13-28-32

This control does not by default set up values. You must do that. Select the bottom attribute,  Right-click the entry, then choose Add Row.  Make the row type Values. Open Values‘ array, which should be empty. Using either the Add row or the + Icon, add four  sub rows to Values. Add the  number  0  to the first  row. Change the type of the row to Number.  Do the same for the other rows, making the values 1,  2 and 3.

2016-03-13_13-28-33

Close the Values array. Add another attribute to the multi-value control Titles. Open the attribute’s array and add four entries. These will be  the strings Small, Medium, Large, and Extra Large.

2016-03-13_13-31-04

We can look at our settings page now.  Just to make it easy to know when we can look at it, set the back ground in the Launchscreen.storyboard to Orange(#ff8000), and the Main.Storyboard to Yellow(#FFFF00). Build and run. When the Background turns yellow, go to the settings app.  If on a live device, hit the Home button. If on the simulator,  hit Command-Shift-H on your keyboard for the home button. Navigate to the settings app.

2016-03-13_13-42-05

Scroll down to the bottom of the app. You’ll find our app there.

2016-03-13_13-42-25

Click the item and we get a settings page.

2016-03-13_13-42-58

Setting up the Storyboard

Stop the app, then go to  Main.storyboard in Xcode. Add a switch, a label, a text field  and  a segmented control to the storyboard. Configure the controls to look like this:

2016-03-13_14-27-04

Select the switch. In the size inspector, set Compression Resistance and Content Hugging to 1000 horizontally and vertically. Switches should never change size, and this prevents that from happening.

2016-03-13_14-36-49

Click the stack view button2016-03-13_14-36-07 to make a horizontal stack view. Select everything on the storyboard, and make vertical stack view by pressing the stack view button2016-03-13_14-36-07 again.  Set the Stack View’s attributes to this:

2016-03-13_14-43-19

Pin the stack view 71 up, 5 left and 5 right. Update frames for items of new constraints.

2016-03-13_14-50-17

Open up the assistant editor and connect up the following outlets to their proper controls:

@IBOutlet weak var roomForCream: UISwitch!
@IBOutlet weak var drinkText: UITextField!
@IBOutlet weak var sizeSegment: UISegmentedControl!

Reading the Settings Bundle

The NSUserDefaults does not know we have a settings bundle. Our first task is to register the settings bundle with NSUserDefaults.

func registerSettingsBundle(){
   let appDefaults = [String:AnyObject]()
   NSUserDefaults.standardUserDefaults().registerDefaults(appDefaults)
}

This registers the settings bundle to the NSUserDefaults standard defaults. the registerDefaults method looks for property lists in the resources directory and changes the value:key entries to dictionaries of the form [String:AnyObject]. We only run this once in our code.

Make a new function updateDisplayFromDefaults and get our defaults

func updateDisplayFromDefaults(){
//Get the defaults
    let defaults = NSUserDefaults.standardUserDefaults()
}

Read the key:value pairs and assign them to our outlets. Add this to the function:

//Set the controls to the default values. 
    roomForCream.on = defaults.boolForKey("coffee_cream")
    if let drink = defaults.stringForKey("coffee_type"){
        drinkText.text = drink
    } else{
        drinkText.text = ""
    }
    sizeSegment.selectedSegmentIndex = defaults.integerForKey("coffee_size")

The stringForKey method returns a optional value. Our code checks for a nil value and reacts accordingly. Add the new function to viewDidload:

override func viewDidLoad() {
    super.viewDidLoad()
    registerSettingsBundle()
    updateDisplayFromDefaults()
 }

We want to start fresh when we load the app. Go to the simulator or your device. Delete the application by pressing and holding on the app until it shakes. Once shaking, click the X to delete it. You will be asked

2016-03-13_15-03-16

Click Delete and the app and the settings are now deleted. Build and run the application. We start with a blank preferences, since they haven’t been written to the app yet. Since the Default Value of the property list is a display value, it does not reflect here.

2016-03-13_15-14-09

Press Command-Shift-H, then navigate over to the settings. Set your settings to this:

2016-03-13_15-20-13

Press Command-Shift-HH to and select the demo app. Our app hasn’t changed.

2016-03-13_15-14-09

Close the app in Xcode. Re-run the app. Now we get our defaults.

2016-03-14_07-56-20

Updating Defaults with Observers

Updating our preferences on restart only  is not a good thing. The problem is we need to tell the app that there was a change. This needs to be done automatically.In both a settings bundle and for NSUserDefaults within an app, this will be a frequent problem. You change a setting and want everywhere that uses it to update. Such changes need notification observers. I think of fire watchers as an analogy to observers. You have a forest ranger sitting in a tower looking for fire in the forest below. If she finds one, then she sets off an alrm to fight the fire to everyone in the forest. An observer does the the same thing in code. It looks for a certain event to happen, then runs a target method if is sees it happening. These events can be system defined events known as notifications (not to be confused with push notifications that show up on your device) or you can make up your own. I’ll discuss how to make notifications in a future post. We’ll concentrate on system generated ones.

The NSUserDefaults class generates a notification NSUserDefaultsDidChangeNotification. We can set an observer for the notification, and then run some code to update the display. Change viewDidLoad to this:

override func viewDidLoad() {
    super.viewDidLoad()
    registerSettingsBundle()
    updateDisplayFromDefaults()
        
    NSNotificationCenter.defaultCenter().addObserver(self,
        selector: "defaultsChanged",
        name: NSUserDefaultsDidChangeNotification,
        object: nil)
    }

We call the default notification center which keeps track of our observers to register it. This center is NSNotificationCenter.defaultCenter(). We add the observer with name NSUserDefaultsDidChangeNotification and tell it to run the function defaultsChanged when it observes a notification.

As an aside, you might be tempted to add the code from registerSettingsBundle as part of your updateDisplayFromDefaults. Every execution of registerSettingsBundle changes the NSUserDefaults by adding more defaults. Each time we add a default we get a notification, causing a recursive death spiral until we run out of memory. Keep it separate.

We’ll need a small function defaultsChanged to run when we have a notification. Add this to your code:

func defaultsChanged(){
        updateDisplayFromDefaults()
    }

There’s also a bit of legacy code we have to add. Prior to iOS9, we need to remove the observer for good memory management. In iOS9 and later, ARC does that for us. For any iOS8 devices, add the following to remove the observer.

deinit { //Not needed for iOS9 and above. ARC deals with the observer.
NSNotificationCenter.defaultCenter().removeObserver(self)
}

The code uses the Swift class’ deinint function to remove the observer from NSNotificationCenter.defaultCenter().

A Bug in the Simulator

Build and run. Hit Command-Shift-H then switch over to the settings app. Select the settings for the SettingsBundleDemo and… It’s blank.

2016-03-13_15-19-17

This doesn’t happen on a device. Only the simulator. There is a workaround. It seems the simulator is still running your last iteration of your app. When you hit stop in Xcode it doesn’t stop the settings app. Click Command-Shift-HH to double-click the home button. Swipe up on the Settings app to kill the process. Start the Settings again, and settings refreshes itself. You’ll have a settings page again. Change the settings to this.

2016-03-14_07-33-44

Go back to the Settings Bundle Demo. Our results show up this time.

2016-03-14_07-34-16

That’s the basics of Settings Bundles. There are a few more advanced options such as child settings pages and coding the settings directly in XML. The charts above have the tags for XML and there is is the Root.plist code below in XML for your further exploration.

The Whole Code

//
//  ViewController.swift
//  SetttingsBundleDemo
//
//  Created by Steven Lipton on 3/11/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var roomForCream: UISwitch!
    @IBOutlet weak var drinkText: UITextField!
    @IBOutlet weak var sizeSegment: UISegmentedControl!
    
    deinit { //Not needed for iOS9 and above. ARC deals with the observer.
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
    
    func registerSettingsBundle(){
        let appDefaults = [String:AnyObject]()
        NSUserDefaults.standardUserDefaults().registerDefaults(appDefaults)
        //NSUserDefaults.standardUserDefaults().synchronize()
    }
    
    func updateDisplayFromDefaults(){
    
        
     
        //Get the defaults
        let defaults = NSUserDefaults.standardUserDefaults()

        //Set the controls to the default values. 
        roomForCream.on = defaults.boolForKey("coffee_cream")
        if let drink = defaults.stringForKey("coffee_type"){
            drinkText.text = drink
        } else{
            drinkText.text = ""
        }
        sizeSegment.selectedSegmentIndex = defaults.integerForKey("coffee_size")
    }

    func defaultsChanged(){
        updateDisplayFromDefaults()
    }
    @IBAction func updateDefaults(sender: AnyObject) {
        updateDisplayFromDefaults()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        registerSettingsBundle()
        updateDisplayFromDefaults()
        NSNotificationCenter.defaultCenter().addObserver(self,
            selector: "defaultsChanged",
            name: NSUserDefaultsDidChangeNotification,
            object: nil)
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}


Root.plist

While we did not discuss them here, you can make entries in XML to your property list. Here’s the property list for the demo in XML:

<?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>PSMultiValueSpecifier</string>
			<key>Title</key>
			<string>Size</string>
			<key>Key</key>
			<string>coffee_size</string>
			<key>DefaultValue</key>
			<string>0</string>
			<key>Values</key>
			<array>
				<integer>0</integer>
				<integer>1</integer>
				<integer>2</integer>
				<integer>3</integer>
			</array>
			<key>Titles</key>
			<array>
				<string>Small</string>
				<string>Medium</string>
				<string>Large</string>
				<string>Extra Large</string>
			</array>
		</dict>
		<dict>
			<key>Type</key>
			<string>PSToggleSwitchSpecifier</string>
			<key>Title</key>
			<string>Room for cream</string>
			<key>Key</key>
			<string>coffee_cream</string>
			<key>DefaultValue</key>
			<true/>
		</dict>
		<dict>
			<key>Type</key>
			<string>PSTextFieldSpecifier</string>
			<key>Title</key>
			<string>Beverage of Choice</string>
			<key>Key</key>
			<string>coffee_type</string>
			<key>DefaultValue</key>
			<string>Coffee</string>
		</dict>
	</array>
</dict>
</plist>

5 responses to “Using Settings Bundles with Swift”

  1. Thanks for the tutorial! Anyway we can make the Setting page an account manager for signin like the Twitter or Facebook setting page inside Settings?

    1. Not sure I’m understanding the question.

  2. This is a nice explanation, but I have an additional request. While it’s great to have a defaults bundle that can be administered from the iPhone’s settings app, I’d really like to give users the choice of editing settings through the settings app or selecting a [menu] option so that the application settings can be displayed and changed in-app. To me, it seems a bit non-intuitive to force application users to go somewhere else to modify settings for their application. I suppose I could create a Table View Controller and duplicate much of the settings bundle user interface code to create an in-app version of my settings bundle, but I’d rather ask if someone else has developed a settings bundle view controller that would simply display the settings bundle within my application?

  3. Great article! One quick question – in this demo you showed all of the logic within the view controller. In a multi-VC app, how would you group the logic? For example, I was wondering if it made more sense to register the settings in AppDelegate, so that the settings will apply to the user upon loading (as opposed to only when user navigates to the settings view controller). Thank you! – Jeff

    1. Really depends on the situation but possible, yes. If I am not using observers, I might check in every relevant viewWillAppear or ViewDidLoad as well. However, remember that AppDelegate only runs at startup. If a user changes a setting while using the app, that won’t reflect as easily, and will need more property observers to keep it all straight. If I am not using observers, I might check in every relevant viewWillAppear or ViewDidLoad as well. That of course has a lot of overhead.

      And of course this all changes with SwiftUI.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: