Xcode上制作系统偏好设置面板项目(Preference Panes)

翻了半天网上没有很多关于这方面的,虽然Apple开发文档里写有。
嘛咱也不是说要翻译这个文档,而是简单介绍一下。
使用prefPane就和iOS的Settings里面的那些第三方程序设置一样,只不过Mac是直接安装到系统偏好设置面板里。而主要的功能就是作为外部设置来设置app,好处就是能变相绕过Sandbox来开启程序内部的一些不被允许的功能,当然同时app因为本身没开启这些功能可以通过Apple的审核上架到MacAppStore。

首先来了解下prefPane的原理,其实prefPane就是一个bundle,和app差不多,同样需要h和m文件,也有info.plist,也需要icon,而UI的话只有一个nib(当然可以做得多个然后之前切换什么的,那些就比较高级了)。而prefPane根据需要可以分成独立的prefPane来执行功能,也可以通过修改目标程序的设置plist文件达到设置app的功能,也可以直接使用NotificationCenter来通知程序:设置已经更改了!快做出反应!
这次主要介绍修改目标程序的偏好设置plist文件,优点是非常简单,缺点就是设置并不能马上生效,必须重新加载读取UserDefault或者重新运行程序。

那么马上开始吧,打开Xcode,新建一个project,然后选择“Preference Pane”。
屏幕快照 2013-10-10 21.00.07
之后为面板创建一个名称,当然最好是目标程序的名称啦~(比如咱的是testpane),最后选个地方保存。

接着转到xib界面文件,然后将File’s Owner的Class把原来的“NSPreferencePane”修改为为程序名称的那个(比如testpane)。
然后就是编写界面了,建议和头文件同时编写,方法和普通程序一样。
屏幕快照 2013-10-10 21.15.15
接下来做好m文件的准备,嘛反正就是将那些IBAction复制过去再修改一下。
屏幕快照 2013-10-10 21.17.37

接下来是添加目标程序相关了,首先在h文件里添加
#import
以及
CFStringRef appID;
大概变成这样:
屏幕快照 2013-10-10 21.19.42
h文件部分现在基本完成了,接下来就是在m文件里添加initWithBundle:

- (id)initWithBundle:(NSBundle *)bundle
{
if ( ( self = [super initWithBundle:bundle] ) != nil ) {
appID = CFSTR("com.mycompany.example");
}
return self;
}

将以上一段直接添加到m文件里,注意com.mycompany.example是目标程序的App ID,也就是info.plist里的Bundle identifier。
接着在mainViewDidLoad里添加以下代码:

FPropertyListRef value;
value = CFPreferencesCopyAppValue( CFSTR("kAutoReload"), appID );
if ( value && CFGetTypeID(value) == CFBooleanGetTypeID() ) {
[self.autoreload setState:CFBooleanGetValue(value)];
} else {
[self.autoreload setState:NO];
}
if ( value ) CFRelease(value);
value = CFPreferencesCopyAppValue( CFSTR("kAuto Shutdown Firewall"), appID );
if ( value && CFGetTypeID(value) == CFBooleanGetTypeID() ) {
[self.autocutfw setState:CFBooleanGetValue(value)];
} else {
[self.autocutfw setState:NO];
}
if ( value ) CFRelease(value);

注意咱这里只拿autoreload和autocutfw这两个checkbox(NSButton)来做示范,对应plist里的设置应该是kAutoReload和kAuto Shutdown Firewall,自己有别的需求可以改成别的。而如果用到NSTextField的话则是:

value = CFPreferencesCopyAppValue( CFSTR("kString Value"), appID );
if ( value && CFGetTypeID(value) == CFStringGetTypeID() ) {
[self.textfield setStringValue:(NSString *)value];
} else {
[self.textfield setStringValue:@""];
}
if ( value ) CFRelease(value);

其他的类似。
目前为止代码看起来应该是这样的:
屏幕快照 2013-10-10 21.30.01
以上就是面板被加载时自动读取plist的设置。

接下来就是按下checkbox之后发生的事情了,其实checkbox就是个NSButton,所以不仅要写好按下之后传输的状态,也别忘记写checkbox的状态变化了。
于是在对应IBAction里添加以下代码:

if ( [sender state] )
CFPreferencesSetAppValue( CFSTR("kAutoReload"),
kCFBooleanTrue, appID );
else
CFPreferencesSetAppValue( CFSTR("kAutoReload"),
kCFBooleanFalse, appID );

注意kAutoReload修改成自己需要的,代码看上去大概是这样的:
屏幕快照 2013-10-10 21.34.03
另外如果使用了NSTextField,那么添加以下代码,当关闭面板时自动保存并通知程序:

- (void)didUnselect
{
CFNotificationCenterRef center;
CFPreferencesSetAppValue( CFSTR("kString Value"),
[self.textfield stringValue], appID );
CFPreferencesAppSynchronize( appID );
center = CFNotificationCenterGetDistributedCenter();
CFNotificationCenterPostNotification(center,
CFSTR("Preferences Changed"), appID, NULL, TRUE);
}

而checkbox并不需要因为checkbox在变量修改时就已经自动保存到plist里了,但是TextField却不能,所以需要手动去保存并同步设置。

至此代码工作基本就完成了,之后就是收尾部分了,比如添加icon以及UI修改等。以下为tips:
1.直接将app的icon的icons或者png图片文件等用系统自带的预览工具导出成tiff然后覆盖projcet目录下的tiff文件就可以直接替换icon了(比如testpane.tiff)。
2.系统偏好设置面板列表上的名称设置是info.plist里的Preference Pane icon label项目。如果修改后没有更新请多覆盖安装或者重新开启面板就可以看到变化了。
3.修改面板顶部title的名称是info.plist的Main nib file base name项目。
4.测试方法:Xcode里Build之后在Products文件架里找到.prefPane文件(比如testPane.prefPane),右键”Show in Finder”,然后会自动在Finder里找到,双击打开然后安装即可。

以上则为最简单的prefPane制作,功能上app里在NSUserDefaults里预留好对应的位置,然后设置其为off让其不能运行,之后提交审核,一般都是能正常通过的(毕竟功能并没有执行,而且因为代码进行加密过Apple也没那精力去反编译,所以通过率应该很大的),通过之后再修改app介绍让用户自行去下载安装prefPane再引导用户设置一下就能开启原本不应该被Sandbox允许的功能了。
至少目前VOX已经是这么做的了~咱没有进行反编译只是针对这个方法设想了一下。好吧咱错了,不过原理基本上差不多,VOX是使用perfPane运行一个VOX Helper.app,prefPane起到一个开关的作用,VOX Helper则是在后台负责进行监听按键(MediaKey、AppleRemote、HeadPhone),然后通过URL等方式传输控制信息给VOX播放器。

如有问题可以留言询问,虽然咱也不一定能帮上忙就是了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

:b1 :b2 :b3 :b4 :b5 :b6 more »

Note: Commenter is allowed to use '@User+blank' to automatically notify your reply to other commenter. e.g, if ABC is one of commenter of this post, then write '@ABC '(exclude ') will automatically send your comment to ABC. Using '@all ' to notify all previous commenters. Be sure that the value of User should exactly match with commenter's name (case sensitive).

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