Dev:Preferences

From The Apple Wiki
This article is about reading and writing preferences. For the Preferences user interface framework, see Preferences.framework.

Tweaks typically have preferences that can be set by the user from the Settings app. The Preferences framework uses the CFPreferences [Archived 2015-12-02 at the Wayback Machine] family of functions to read and write preferences. Before OS X 10.8 and iOS 8, these functions would directly read and write a plist with the name of the application identifier in ~/Library/Preferences. (Note that when an app is sandboxed in a container, the home directory is the path to the container, not the user's home directory.) Nowadays, preferences are managed by a daemon called cfprefsd.

The Foundation class NSUserDefaults provides a subset of features over the CFPreferences functions. It appears to be intended more as a simple-to-use class that handles calling CFPreferences functions for you, as opposed to being feature-by-feature compatible with CFPreferences.

CFPreferences functions can take and return CFStringRefs and CFPropertyListRefs [Archived 2011-03-05 at the Wayback Machine]. As such, you can toll-free bridge these from or to their matching Foundation classes.

cfprefsd

cfprefsd is the daemon that handles reading and writing preference values. It also caches values in memory so that they can be returned faster. Its goal is to avoid disk reads/writes as much as is possible. This has caused some confusion among tweak developers who have for the most part simply been reading their preference plist directly from the disk, which isn't written to until cfprefsd decides to flush its in-memory cache, or it's sent a SIGTERM (killall cfprefsd). It was introduced to OS X with 10.8, and to iOS with 8.0.

Due to cfprefsd not working as desired in processes that live in an app container and having no documentation other than a vague man page [Archived 2015-09-23 at the Wayback Machine], various developers have created different solutions to read and write preference values. These are outlined at PreferenceBundles § Loading Preferences.

The signal SIGUSR1 can be sent to cfprefsd to make it write a file to /tmp. It provides data about each preference domain read or written to using CFPreferences APIs since the daemon started, in the following form:

$ killall -USR1 cfprefsd
$ head -24 '/tmp/cfprefsddump(166:460692319.078117).txt'

*****************************************************
Domain: fud
User: root
Container: (null)
Path: /Library/Managed Preferences/root/fud.plist
plist data:(null)
shmem index:2017
dirty:0
byHost:1
mode:600
isMultiProcess:1

*****************************************************
Domain: com.apple.WebKit.Networking
User: mobile
Container: (null)
Path: /var/mobile/Library/Preferences/com.apple.WebKit.Networking.plist
plist data:(null)
shmem index:1145
dirty:0
byHost:0
mode:600
isMultiProcess:1

Examples

To do: read this carefully and move all information regarding preferences to a more appropriate place. Bring the following contents:

Tweak part

As opposed to using NSDictionaries and [NSHomeDirectory() stringByAppendingFormat:@"/Library/Preferences/%s.plist", "com.your.tweak"], the following implementation uses NSUserDefaults, which works properly for unsandboxed processes, so read the previous links to handle preferences on sandboxed processes until an implementation is added here.

Tweak.xm
#import <Foundation/NSUserDefaults+Private.h>

static NSString *nsDomainString = @"com.your.tweak";
static NSString *nsNotificationString = @"com.your.tweak/preferences.changed";
static BOOL enabled;

static void notificationCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
	NSNumber *n = (NSNumber *)[[NSUserDefaults standardUserDefaults] objectForKey:@"enabled" inDomain:nsDomainString];
	enabled = (n) ? [n boolValue] : YES;
}

%ctor {
	// Set variables on start up
	notificationCallback(NULL, NULL, NULL, NULL, NULL);

	// Register for 'PostNotification' notifications
	CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
		NULL,
		notificationCallback,
		(CFStringRef)nsNotificationString,
		NULL,
		CFNotificationSuspensionBehaviorCoalesce);

	// Add any personal initializations
}

/*
 * From here onward, write your tweak.
 * To make your tweak actually do stuff when enabled:
	if (!enabled) {
		// Do the original algorithm by calling:
		// %orig();
	} else {
		...
		// Optionally, do the original algorithm
	}
 */

Preference plist

Choose any of the following styles, see preferences specifier plist for all available specifier types and how to use them.

Reduced style

Provides a switch on the root section of the preferences (like Airplane Mode). Recommended for configuration-less tweaks.

Saved in your tweak's folder as layout/Library/PreferenceLoader/Preferences/com.your.tweak/entry.plist

entry.plist
NeXTSTEP style
{
	entry = {
		cell = PSSwitchCell;
		defaults = "com.your.tweak";
		label = "Your Tweak";
		key = enabled;
		default = 1;
		icon = "/Applications/Preferences.app/icon-table@2x.png";
		PostNotification = "com.your.tweak/preferences.changed";
	};
}
XML style
<?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>entry</key>
	<dict>
		<key>cell</key>
		<string>PSSwitchCell</string>
		<key>defaults</key>
		<string>com.your.tweak</string>
		<key>label</key>
		<string>Your Tweak</string>
		<key>key</key>
		<string>enabled</string>
		<key>default</key>
		<true/>
		<key>icon</key>
		<string>/Applications/Preferences.app/icon-table@2x.png</string>
		<key>PostNotification</key>
		<string>com.your.tweak/preferences.changed</string>
	</dict>
</dict>
</plist>

Extended Style

Provides a pane where other cells can appear (like Wi-Fi). Recommended for configuration-friendly tweaks.

Saved in your tweak's folder as layout/Library/PreferenceLoader/Preferences/com.your.tweak/entry.plist

entry.plist
NeXTSTEP style
{
	title = "Your Tweak";
	entry = {
		cell = PSLinkCell;
		label = "Your Tweak";
		icon = "/Applications/Preferences.app/icon-table@2x.png";
	};
	items = (
		{
			cell = PSSwitchCell;
			defaults = "com.your.tweak";
			label = Enabled;
			key = enabled;
			default = 1;
			PostNotification = "com.your.tweak/preferences.changed";
		}
		// add more cells (dictionaries) here 
 	);
}
XML style
<?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>title</key>
	<string>Your Tweak</string>
	<key>entry</key>
	<dict>
		<key>cell</key>
		<string>PSLinkCell</string>
		<key>label</key>
		<string>Your Tweak</string>
		<key>icon</key>
		<string>/Applications/Preferences.app/icon-table@2x.png</string>
	</dict>
	<key>items</key>
	<array>
		<dict>
			<key>cell</key>
			<string>PSSwitchCell</string>
			<key>defaults</key>
			<string>com.your.tweak</string>
			<key>label</key>
			<string>Enabled</string>
			<key>key</key>
			<string>enabled</string>
			<key>default</key>
			<true/>
			<key>PostNotification</key>
			<string>com.your.tweak/preferences.changed</string>
		</dict>
		<!-- add more cells (dictionaries) here -->
	</array>
</dict>
</plist>

Inside PreferenceBundles

Provides a static list of cells. Recommended for Preference Bundles of tweaks.

Saved in your tweak's Preference Bundle subproject folder as Resources/com.your.tweak.plist

com.your.tweak.plist
NeXTSTEP style
{
	items = (
		{
			cell = PSSwitchCell;
			defaults = "com.your.tweak";
			label = Enabled;
			key = enabled;
			default = 1;
			PostNotification = "com.your.tweak/preferences.changed";
		}
		// add more cells (dictionaries) here
	);
}
XML style
<?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>PSSwitchCell</string>
			<key>defaults</key>
			<string>com.your.tweak</string>
			<key>label</key>
			<string>Enabled</string>
			<key>key</key>
			<string>enabled</string>
			<key>default</key>
			<true/>
			<key>PostNotification</key>
			<string>com.your.tweak/preferences.changed</string>
		</dict>
		<!-- add more cells (dictionaries) here -->
	</array>
</dict>
</plist>

Flipswitches

After using the Flipswitch NIC template, modify accordingly

Switch.x
#import "FSSwitchDataSource.h"
#import "FSSwitchPanel.h"

@interface NSUserDefaults (Tweak_Category)
- (id)objectForKey:(NSString *)key inDomain:(NSString *)domain;
- (void)setObject:(id)value forKey:(NSString *)key inDomain:(NSString *)domain;
@end

static NSString *nsDomainString = @"com.your.tweak";
static NSString *nsNotificationString = @"com.your.tweak/preferences.changed";

@interface YourTweakFlipswitchSwitch : NSObject <FSSwitchDataSource>
@end

@implementation YourTweakFlipswitchSwitch

- (NSString *)titleForSwitchIdentifier:(NSString *)switchIdentifier {
	return @"Your Tweak";
}

- (FSSwitchState)stateForSwitchIdentifier:(NSString *)switchIdentifier {
	NSNumber *n = (NSNumber *)[[NSUserDefaults standardUserDefaults] objectForKey:@"enabled" inDomain:nsDomainString];
	BOOL enabled = (n)? [n boolValue]:YES;
	return (enabled) ? FSSwitchStateOn : FSSwitchStateOff;
}

- (void)applyState:(FSSwitchState)newState forSwitchIdentifier:(NSString *)switchIdentifier {
	switch (newState) {
	case FSSwitchStateIndeterminate:
	break;
	case FSSwitchStateOn:
		[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES] forKey:@"enabled" inDomain:nsDomainString];
		CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (CFStringRef)nsNotificationString, NULL, NULL, YES);
	break;
	case FSSwitchStateOff:
		[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"enabled" inDomain:nsDomainString];
		CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (CFStringRef)nsNotificationString, NULL, NULL, YES);
	break;
	}
	return;
}

@end