Preferences

There are 2 types of user preferences provided, additional options that can be configured once when widgets are added to the page and settings options that can be configured anytime via settings accessory. Convenience classes are provided for both of these, including a class that can be shared for both cases, to make the process easier.

Additional Options

Before digging into how to implement additional options, lets go over their lifecycle beginning with how they are constructed. When widget is tapped in HSWidgets' adding view is displayed, it'll initially check if the widget provides a valid additional options class. Valid class being a view controller that conforms to HSWidgetAdditionalOptions. Once it has determine the validiy of class, it 'll try to construct an instance of it using one of the initializers (in that order):

  1. Optional initializer of above mentioned protocol, initWithWidgetsOptionsToExclude:withDelegate:availablePositions:
  2. Initializer of PSListController (yes we can use any class from the Preference framework but we'll go more into that later), initForContentSize:
  3. Fallback to initializer of UIViewController, init

After construction, It'll be pushed onto navigation controller stack and go through the view controller life cycle. Additional options class is then responsible for setting up widgetOptions as options change. Once all options have been configured by the user, additional options class should call additionalOptionsViewController:addWidgetForClass: on its delegate. The implementation of this method will call createOptionsFromController:withAvailableGridPosition: on the widget to allow it to do any conversions to dictionary (that can be serialized). Finally, it'll construct widget view controller using the options dictionary.

These responsibilities become easier to manage with provided convenience classes. There are 2 classes available, HSWidgetAdditionalOptionsViewController (additonal options view controller) and HSWidgetCombinedAdditionalOptionsAndPreferencesViewController (combined additional options view controller), both useful under different circumstances. Additional options view controller is a table view controller so it should be used when user needs to select from a list, specially when that list is dynamic. Combined additional options view controller, on the other hand, is a PSListController and conforms to HSWidgetPreferences. This means we can use the same plist approach for generating the options as Preference framework. And because it conforms to HSWidgetPreferences, we can use the same class for settings options. For most cases, it is recommended to use the combined additional options view controller.

To add additional options to our widget, we would create a subclass of one of the above mentioned classes and then add HSWidgetAddNewOptionsControllerClass to Resources/Info.plist.


<key>HSWidgetAddNewOptionsControllerClass</key>
<string>SUBCLASSNAME</string>
Replace SUBCLASSNAME with the name of subclass that you created for additional options.

Settings

Settings options should be used for adding user options that can be configured at anytime. It is preferred to use this over using preference bundles that are displayed in Settings app. This is because settings options are easier to access and provide individual customizability per widget instance via convenience classes. But before getting into the classes, lets go over their lifecycle. It begins with the tap event on the settings accessory. Since there are no restrictions on the type/inheritance of the class, HSWidgets does quite a bit of checking before constructing the preference view controller. It follows the following logic:

  1. If preference view controller conforms to HSWidgetPreferences then use initializer specified by the protocol, initWithWidgetViewController:availablePositions:
  2. Initializer of PSListController if implemented, initForContentSize:
  3. Fallback to initializer of UIViewController, init

This will then be pushed onto navigation controller stack and go through rest of the view controller lifecycle. The preference view controller is responsible for saving and updating the widget. However both of these become easier with the provided classes, HSWidgetPreferencesListController (preferences list controller) and before mentioned HSWidgetCombinedAdditionalOptionsAndPreferencesViewController. Since both of these are PSListController we can construct our preferences the same way we construct preference bundles by using plist approach.

So to enable settings options, we need to enable settings accessory which trigger them. By default this accessory is enabled when a valid class is set for HSWidgetPreferencesControllerClass in Resources/Info.plist.


<key>HSWidgetPreferencesControllerClass</key>
<string>SUBCLASSNAME</string>
Replace SUBCLASSNAME with the name of subclass that you created for settings options.

Adding Preferences to Square Widget

Lets add simple preferences to our square widget. To keep things simple, we will add preferences for picking color from a list (segment cell). Lets allow the user to configure this when widget is being added and anytime via the settings accessory so lets subclass HSWidgetCombinedAdditionalOptionsAndPreferencesViewController. For the subclass we need header file with:


#import <HSWidgets/HSWidgetCombinedAdditionalOptionsAndPreferencesViewController.h>

@interface HSCustomWidgetPreferencesViewController : HSWidgetCombinedAdditionalOptionsAndPreferencesViewController
@end

Then we need to implement the plist file that has the segment cell for picking the color, Resources/Root.plist:


<?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>items</key>
	<array>
		<dict>
			<key>cell</key>
			<string>PSGroupCell</string>
			<key>label</key>
			<string>Color</string>
			<key>footerText</key>
			<string>Pick the color of the square</string>
		</dict>
		<dict>
			<key>cell</key>
			<string>PSSegmentCell</string>
			<key>key</key>
			<string>WidgetColor</string>
			<key>validTitles</key>
			<array>
				<string>Red</string>
				<string>Green</string>
				<string>Blue</string>
			</array>
			<key>validValues</key>
			<array>
				<integer>0</integer><!-- Red -->
				<integer>1</integer><!-- Green -->
				<integer>2</integer><!-- Blue -->
			</array>
			<key>default</key>
			<integer>0</integer>
		</dict>
	</array>
	<key>title</key>
	<string>Square</string>
</dict>
</plist>

This defines red color as 0, green color as 1 and blue color as 2 which we will need later. Now that we have setup our plist, we need to load the specifiers in our view controller using loadSpecifiersFromPlistName:. So in our implementation of the subclass, we would have:


#import "HSCustomWidgetPreferencesViewController.h"

@implementation HSCustomWidgetPreferencesViewController
-(NSArray *)specifiers {
	if (!_specifiers) {
		_specifiers = [self loadSpecifiersFromPlistName:@"Root" target:self];
	}
	return _specifiers;
}
@end

Then modify our makefile to also compile the new implementation file along with our main widget class and link against Preferences framework since we are using specifiers.


HSCustomWidget_FILES = HSCustomWidgetViewController.mm HSCustomWidgetPreferencesViewController.mm
HSCustomWidget_PRIVATE_FRAMEWORKS = Preferences

The final steps is to let HSWidgets know to use this class for additional options and then use the preference options in our widget. As mentioned above:

To add additional options to our widget, we would create a subclass of one of the above mentioned classes and then add HSWidgetAddNewOptionsControllerClass to Resources/Info.plist.

We have already subclassed it, we just need to add the class to Info.plist.


<key>HSWidgetAddNewOptionsControllerClass</key>
<string>HSCustomWidgetPreferencesViewController</string>

This will now make it so that our additional options view controller is displayed when Custom Widget is tapped in HSWidgets' adding view.

But we still need to update our widget to use the value, so how do we access the user selected options? Well the options are passed into the widget's initializer and also stored into widgetOptions instance variable in the super class. Since we are creating our view after the initializer, we would use the instance variable to get the value. In viewDidLoad, we want to instead set the color using:


NSInteger colorPicked = [widgetOptions[@"WidgetColor"] integerValue];
if (colorPicked == 0)
	self.square.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.5];
else if (colorPicked == 1)
	self.square.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.5];
else if (colorPicked == 2)
	self.square.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.5];

Deploying it on device, we will now see the correct color being added. But what if the user wants to change the color? With our current approach they would need to delete the widget and re-add it with the new color which is horrible UX. We instead should enable settings options and update widget's color as options change.

To enable settings options, we need to enable settings accessory which trigger them. By default this accessory is enabled when a valid class is set for HSWidgetPreferencesControllerClass in Resources/Info.plist.

Since we subclassed the combined list view controller, we can use the same class for HSWidgetAddNewOptionsControllerClass and HSWidgetPreferencesControllerClass.


<key>HSWidgetPreferencesControllerClass</key>
<string>HSCustomWidgetPreferencesViewController</string>

The settings accessory will now be enabled and will show along with our other accessories.

Before getting into updating our widget's color, we should look into how those values will be read/stored/updated by the conenience classes. Both HSWidgetPreferencesListController and HSWidgetCombinedAdditionalOptionsAndPreferencesViewController override the setter and getter method of the specifier (setPreferenceValue:specifier: and readPreferenceValue: respectively). The implementation of reading directly gets value from widget's options. For setting/updating, setWidgetOptionValue:forKey: is called on widget with the value being the NSNumber/NSString/etc and key being the specifier's key property value. This means that to update our content we can override this method in our widget view controller.


-(void)setWidgetOptionValue:(id<NSCoding>)object forKey:(NSString *)key {
	[super setWidgetOptionValue:object forKey:key];

	if ([key isEqualToString:@"WidgetColor"]) {
		NSInteger colorPicked = [widgetOptions[@"WidgetColor"] integerValue];
		if (colorPicked == 0)
			self.square.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.5];
		else if (colorPicked == 1)
			self.square.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.5];
		else if (colorPicked == 2)
			self.square.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.5];
	}
}

Now we will be able to change the colors via the settings accessory.

Universal Settings

You might have noticed that these settings are being applied individually per widget which is great but might not always be what we want. It could be helpful to instead have an option to have universal settings for all widgets. To do that, we would instead have to change setter/getter of the specifier to read/update a universally stored value. HSWidgetPreferencesListController provides a setter/getter for NSUserDefaults and for file that can be used. For most cases the provided methods should be enough so we only need to update the setter/getter of the specifier. The easiest way of doing this is to set them in our plist:


<key>get<\key>
<string>readUserDefaultsValue:<\string>
<key>set<\key>
<string>setUserDefaultsValue:specifier:<\string>
Remember to also set defaults and PostNotification. PostNotifications are posted on default NSNotificationCenter instead of darwin notifications as they don't need to registered outside this process. This also makes it easier to use in Objective-C classes.

Now we would just need to register for the notification and update the content based on the value. I'll leave that up to you to pratice.