PreferenceBundles: Difference between revisions

From iPhone Development Wiki
m (Added to Preferences category)
(HBPreferences.)
 
(31 intermediate revisions by 13 users not shown)
Line 25: Line 25:


{{main|PreferenceLoader#PreferenceBundle_Approach}}
{{main|PreferenceLoader#PreferenceBundle_Approach}}
When creating these bundles you might use other interface elements than PSCells. When doing so you can have some that modify the preferences so you'll want to make those changes be saved. For this objective there are various APIs; aside from the {{ObjcCall|NSDictionary|writeToFile:atomically:}}, there's the CFPreferences family of functions. The following snippet shows how to save a dictionary to a "domain":
<source lang="objc">
Boolean savePreferencesDictionary(CFStringRef appID, CFDictionaryRef dict) {
CFPreferencesSetMultiple(dict, nil, appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
return CFPreferencesSynchronize(appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
}
/*
* call from Foundation:
NSString *bundleID = @"com.your.tweak";
NSDictionary *preferences = @{ @"enabled":@YES, @"title":@"An Awesome Tweak!"};
BOOL b = (BOOL)savePreferencesDictionary((CFStringRef)bundleID, (CFDictionaryRef)preferences);
*/
</source>
== Loading Preferences ==
=== The old simple way ===


It is very common to load preferences in the constructor (<code>%ctor</code>) of your tweak.  
It is very common to load preferences in the constructor (<code>%ctor</code>) of your tweak.  
Line 42: Line 62:
</source>
</source>


It is very important to only load the preferences in the constructor and not access or modify any UI elements. If you need to do this, you can load your preferences in SpringBoard's init method.  
It is very important to only load the preferences in the constructor and not access or modify any UI elements. If you need to do this, register a callback for <code>UIApplicationDidFinishLaunchingNotification</code> to do UI related operations in.
 
More information about preferences can be seen [[Preferences#Examples | here]].
 
=== Into sandboxed/unsandboxed processes in iOS 8 ===
 
This is a method that Karen (angelXwind) uses for several of her tweaks, notably mikoto and [https://github.com/angelXwind/PreferenceOrganizer2/ PreferenceOrganizer 2] as of this writing.
 
Her method involves overriding setPreferenceValue:specifier and readPreferenceValue: in the preference bundle to restore the old, pre-iOS 8 behaviour as it completely bypasses CFPreferences and writes directly to file.
 
This way, you can continue to read from the plist without worrying about cfprefsd. CFNotifications are still posted upon preference set.
 
This method has been tested to work in iOS 5, 6, 7, and 8.
 
Add this in your PSListController implementation code:
 
<source lang="objc">
 
- (id)readPreferenceValue:(PSSpecifier*)specifier {
NSString *path = [NSString stringWithFormat:@"/User/Library/Preferences/%@.plist", specifier.properties[@"defaults"]];
NSMutableDictionary *settings = [NSMutableDictionary dictionary];
[settings addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile:path]];
return (settings[specifier.properties[@"key"]]) ?: specifier.properties[@"default"];
}
 
- (void)setPreferenceValue:(id)value specifier:(PSSpecifier*)specifier {
NSString *path = [NSString stringWithFormat:@"/User/Library/Preferences/%@.plist", specifier.properties[@"defaults"]];
NSMutableDictionary *settings = [NSMutableDictionary dictionary];
[settings addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile:path]];
[settings setObject:value forKey:specifier.properties[@"key"]];
[settings writeToFile:path atomically:YES];
CFStringRef notificationName = (CFStringRef)specifier.properties[@"PostNotification"];
if (notificationName) {
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, NULL, NULL, YES);
}
}
</source>
 
and also make sure to '''#import <Preferences/PSSpecifier.h>''' in your PSListController's header file
 
=== Into unsandboxed processes (using CFPreferences) ===
 
With the release of iOS 8, it became evident that the popular plist loading method wasn't the best way to load preferences. saurik summarized it well: "As far as I can tell, the idea is that the plist file on disk is simply backing a shared memory region managed by cfprefsd, which Apple has brought to iOS from OS X 10.8. It only gets flushed when cfprefsd "feels like it". But if you ask cfprefsd for the value using the actual APIs you are supposed to use to access these files, it should work."
 
These "actual APIs" are documented here: https://developer.apple.com/library/mac/documentation/CoreFoundation/Reference/CFPreferencesUtils/. Perhaps you'll end up with something like this:
 
<source lang=objc>
CFPreferencesAppSynchronize(CFSTR("com.my.tweak"));
CFPropertyListRef value = CFPreferencesCopyAppValue(CFSTR("enabled"), CFSTR("com.my.tweak"));
//do something with the value
</source>
 
This was tested back to iOS 6, and it seemed to work without problems. '''This solution does not work if you are in third party apps or other apps that have sandboxed preferences.'''
 
The following is an alternative discovered by merdok, which lets you interact with it via dictionary API. It has the same limitations.
 
<source lang="objc">
static NSDictionary *preferences;
 
static void PreferencesChangedCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
[preferences release];
CFStringRef appID = CFSTR("com.my.tweak");
CFArrayRef keyList = CFPreferencesCopyKeyList(appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
if (!keyList) {
NSLog(@"There's been an error getting the key list!");
return;
}
preferences = (NSDictionary *)CFPreferencesCopyMultiple(keyList, appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
if (!preferences) {
NSLog(@"There's been an error getting the preferences dictionary!");
}
CFRelease(keyList);
}
</source>
 
''CFPreferencesCopyMultiple returns a CFDictionaryRef which is "toll-free bridged" with its Cocoa Foundation counterpart, NSDictionary.''
''CFArrayRef keyList = CFAutorelease(CFPreferencesCopyKeyList(appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost)); - throws an error ([http://stackoverflow.com/questions/19229478/cfautorelease-like-behavior-on-ios6#comment28474354_19229478 maybe because it's undocumented]).''
 
(This works with the old dict objectForKey - instead <code>preferences = [[NSDictionary alloc] initWithContentsOfFile:PreferencesPath/com.my.tweak.plist];</code> you can use the above code. This solution doesn't require any massive code modifications to support iOS8)
 
=== Hooking both sandboxed and unsandboxed processes (CFPreferences and NSDictionary) ===
 
<source lang="objc">
static void reloadPrefs() {
    // Check if system app (all system apps have this as their home directory). This path may change but it's unlikely.
    BOOL isSystem = [NSHomeDirectory() isEqualToString:@"/var/mobile"];
    // Retrieve preferences
    NSDictionary* prefs = nil;
    if(isSystem) {
        CFArrayRef keyList = CFPreferencesCopyKeyList(CFSTR("com.your.tweak"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
        if(keyList) {
            prefs = (NSDictionary *)CFPreferencesCopyMultiple(keyList, CFSTR("com.your.tweak"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
            if(!prefs) prefs = [NSDictionary new];
            CFRelease(keyList);
        }
    }
    if (!prefs) {
        prefs = [NSDictionary dictionaryWithContentsOfFile:@"/User/Library/Preferences/com.your.tweak.plist"];
    }
}
</source>


More information about preferences can be seen [http://iphonedevwiki.net/index.php/User:Uroboro#How_2_prefs here].
=== HBPreferences ===


== References ==
<code>HBPreferences</code> is a subclass of <code>NSUserDefaults</code>, a part of <code>libcephei</code> (Cephei) and an all-in-one solution for preferences loading and saving, maintained by [https://github.com/hbang HASHBANG Productions]. It works from both sandboxed and unsanboxed applications while respecting cfprefsd's behavior on those iOS versions that support. A dedicated usage guide can be found in the [https://hbang.github.io/libcephei/Classes/HBPreferences.html Cephei documentation].


* [http://www.touchrepo.com/guides/preferencebundles/PreferenceBundles.doc iPhone Settings Within Settings.app], by Skylar Cantu.
== External links ==


{{Navbox Library}}
{{Navbox Library}}
[[Category:Directories in /System/Library]]
[[Category:Directories in /System/Library]]
[[Category:Preferences]]
[[Category:Preferences]]

Latest revision as of 07:06, 2 December 2018

Preference Bundles are bundles for extending the Settings application. Developers can build their own bundles and place them in /Library/PreferenceBundles/ for others to use.

Structure of a Preference Bundle

Preference bundles must have the extension .bundle. The principle class of the bundle should be a subclass of PSListController or PSViewController. When providing localization files, if a specifier plist is called spec.plist, there should be a corresponding localization file called spec.strings. The bundle can have a 29×29 icon, with a preferred name of icon.png.

For more information on specifiers, see Preferences Specifier Plist Format.

Issues with OS 3.2 and 4.0

PSViewController underwent a massive change after 3.1, breaking all custom subclasses on the iPad and on 4.0 - it is now a UIViewController.

Improper implementations of PSListController subclasses will fail to work properly on 4.0 and later. You must set _specifiers within the - (id)specifiers method and return it. This is because PSListController relies on _specifiers to generate specifier metadata and group indices since iOS 4.0. Example:

- (id)specifiers {
	if (!_specifiers){
		_specifiers = [[self loadSpecifiersFromPlistName: kNameOfPreferencesPlist target: self] retain];
	}
	return _specifiers;
}

Using a Preference Bundle

When creating these bundles you might use other interface elements than PSCells. When doing so you can have some that modify the preferences so you'll want to make those changes be saved. For this objective there are various APIs; aside from the -[NSDictionary writeToFile:atomically:], there's the CFPreferences family of functions. The following snippet shows how to save a dictionary to a "domain":

Boolean savePreferencesDictionary(CFStringRef appID, CFDictionaryRef dict) {
	CFPreferencesSetMultiple(dict, nil, appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
	return CFPreferencesSynchronize(appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
}

/*
 * call from Foundation:
	NSString *bundleID = @"com.your.tweak";
	NSDictionary *preferences = @{ @"enabled":@YES, @"title":@"An Awesome Tweak!"};
	BOOL b = (BOOL)savePreferencesDictionary((CFStringRef)bundleID, (CFDictionaryRef)preferences);
 */

Loading Preferences

The old simple way

It is very common to load preferences in the constructor (%ctor) of your tweak.

static void loadPrefs() {
	NSMutableDictionary *settings = [[NSMutableDictionary alloc] initWithContentsOfFile:@"/var/mobile/Library/Preferences/bundleID.plist"];

	logging = [settings objectForKey:@"logging_enabled"] ? [[settings objectForKey:@"logging_enabled"] boolValue] : NO;
}

%ctor {
    loadPrefs();
    CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)loadPrefs, CFSTR("bundleID/saved"), NULL, CFNotificationSuspensionBehaviorCoalesce);

}

It is very important to only load the preferences in the constructor and not access or modify any UI elements. If you need to do this, register a callback for UIApplicationDidFinishLaunchingNotification to do UI related operations in.

More information about preferences can be seen here.

Into sandboxed/unsandboxed processes in iOS 8

This is a method that Karen (angelXwind) uses for several of her tweaks, notably mikoto and PreferenceOrganizer 2 as of this writing.

Her method involves overriding setPreferenceValue:specifier and readPreferenceValue: in the preference bundle to restore the old, pre-iOS 8 behaviour as it completely bypasses CFPreferences and writes directly to file.

This way, you can continue to read from the plist without worrying about cfprefsd. CFNotifications are still posted upon preference set.

This method has been tested to work in iOS 5, 6, 7, and 8.

Add this in your PSListController implementation code:

- (id)readPreferenceValue:(PSSpecifier*)specifier {
	NSString *path = [NSString stringWithFormat:@"/User/Library/Preferences/%@.plist", specifier.properties[@"defaults"]];
	NSMutableDictionary *settings = [NSMutableDictionary dictionary];
	[settings addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile:path]];
	return (settings[specifier.properties[@"key"]]) ?: specifier.properties[@"default"];
}

- (void)setPreferenceValue:(id)value specifier:(PSSpecifier*)specifier {
	NSString *path = [NSString stringWithFormat:@"/User/Library/Preferences/%@.plist", specifier.properties[@"defaults"]];
	NSMutableDictionary *settings = [NSMutableDictionary dictionary];
	[settings addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile:path]];
	[settings setObject:value forKey:specifier.properties[@"key"]];
	[settings writeToFile:path atomically:YES];
	CFStringRef notificationName = (CFStringRef)specifier.properties[@"PostNotification"];
	if (notificationName) {
		CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, NULL, NULL, YES);
	}
}

and also make sure to #import <Preferences/PSSpecifier.h> in your PSListController's header file

Into unsandboxed processes (using CFPreferences)

With the release of iOS 8, it became evident that the popular plist loading method wasn't the best way to load preferences. saurik summarized it well: "As far as I can tell, the idea is that the plist file on disk is simply backing a shared memory region managed by cfprefsd, which Apple has brought to iOS from OS X 10.8. It only gets flushed when cfprefsd "feels like it". But if you ask cfprefsd for the value using the actual APIs you are supposed to use to access these files, it should work."

These "actual APIs" are documented here: https://developer.apple.com/library/mac/documentation/CoreFoundation/Reference/CFPreferencesUtils/. Perhaps you'll end up with something like this:

CFPreferencesAppSynchronize(CFSTR("com.my.tweak"));
CFPropertyListRef value = CFPreferencesCopyAppValue(CFSTR("enabled"), CFSTR("com.my.tweak"));
//do something with the value

This was tested back to iOS 6, and it seemed to work without problems. This solution does not work if you are in third party apps or other apps that have sandboxed preferences.

The following is an alternative discovered by merdok, which lets you interact with it via dictionary API. It has the same limitations.

static NSDictionary *preferences;

static void PreferencesChangedCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
	[preferences release];
	CFStringRef appID = CFSTR("com.my.tweak");
	CFArrayRef keyList = CFPreferencesCopyKeyList(appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
	if (!keyList) {
		NSLog(@"There's been an error getting the key list!");
		return;
	}
	preferences = (NSDictionary *)CFPreferencesCopyMultiple(keyList, appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
	if (!preferences) {
		NSLog(@"There's been an error getting the preferences dictionary!");
	}
	CFRelease(keyList);
}

CFPreferencesCopyMultiple returns a CFDictionaryRef which is "toll-free bridged" with its Cocoa Foundation counterpart, NSDictionary. CFArrayRef keyList = CFAutorelease(CFPreferencesCopyKeyList(appID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost)); - throws an error (maybe because it's undocumented).

(This works with the old dict objectForKey - instead preferences = [[NSDictionary alloc] initWithContentsOfFile:PreferencesPath/com.my.tweak.plist]; you can use the above code. This solution doesn't require any massive code modifications to support iOS8)

Hooking both sandboxed and unsandboxed processes (CFPreferences and NSDictionary)

static void reloadPrefs() {
    // Check if system app (all system apps have this as their home directory). This path may change but it's unlikely.
    BOOL isSystem = [NSHomeDirectory() isEqualToString:@"/var/mobile"];
    // Retrieve preferences
    NSDictionary* prefs = nil;
    if(isSystem) {
        CFArrayRef keyList = CFPreferencesCopyKeyList(CFSTR("com.your.tweak"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
        if(keyList) {
            prefs = (NSDictionary *)CFPreferencesCopyMultiple(keyList, CFSTR("com.your.tweak"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
            if(!prefs) prefs = [NSDictionary new];
            CFRelease(keyList);
        }
    }
    if (!prefs) {
        prefs = [NSDictionary dictionaryWithContentsOfFile:@"/User/Library/Preferences/com.your.tweak.plist"];
    }
}

HBPreferences

HBPreferences is a subclass of NSUserDefaults, a part of libcephei (Cephei) and an all-in-one solution for preferences loading and saving, maintained by HASHBANG Productions. It works from both sandboxed and unsanboxed applications while respecting cfprefsd's behavior on those iOS versions that support. A dedicated usage guide can be found in the Cephei documentation.

External links