开发Mac上的App插件
文章目录
【注意】最后更新于 February 7, 2017,文中内容可能已过时,请谨慎使用。
在以前的 OS X 系统中,给你的 App 在运行时动态载入可执行代码比较困难。现在可以通过NSBundle
和 plug-ins
插件,可以很方便的向原有APP中添加新功能点。
目的:在一个修改过的 TextEdit 里面加入加载 bundle 的功能
包 (Bundles) 和接口 (Interfaces)
如果你打开 Xcode8 创建一个新项目,会看见 OS X 所有可以编写APP插件的模版,例如: Screen Savers
到 Image Units
等。
在Framework & Library
中的 Bundle
条目。我会在今天探索一个非常简单的的项目,那就是在一个修改过的 TextEdit 里面加入加载 bundle 的功能。
bundle 模版
项目与APP项目比较:
- 一个
Contents
目录,里面包含了Info.plist
和Resource
目录。 - 如果你在你的项目下加入了新的类,你可以看见包含一个可执行文件的
MacOS
目录。 Bundle
工程里缺少的一个东西是main()
函数。它是被宿主App
调用执行的。
为 TextEdit 加入 Plugin 支持
两种插件的方式: 第一个:用最少的工作来为你的 app 加入插件支持,希望让你知道实现这个有多简单。 第二个:技术有点复杂,它展现来一个为你的 app 加入插件的合理的方式,这可以使你不会在未来陷入到被锁死在某一种实现的窘境中。 本文章的项目文件仍然会放在 GitHub 供大家参考。
在 TextEdit 中扫描 Bundle
请打开 “01 TextEdit” 目录下面的 TextEdit.xcodeproj
工程,同时浏览它里面包含的代码。
TextEdit
里面有三个简单的组成部分:扫描 bundle
,加载 bundle
,调用 bundle 的 UI
loadPlugins
方法:
打开 Controller.m
,你可以看见 -(void)loadPlugins
方法 (它在 applicationDidFinishLaunching:
中被调用)。
- 扩展插件菜单:在界面菜单右侧加入了一个新的
NSMenuItem
,为调用插件提供一个入口(通常你会在MainMenu.xib
做这件事情并且链接outlets
,但是我们这次偷下懒)。 - 扫描插件目录:获得插件目录(在
~/Library/Application Support/Text Edit/Plug-Ins/
)下,并且扫描这个目录。 {% codeblock lang:objc %} NSString *pluginsFolder = [self pluginsFolder]; NSFileManager *fm = [NSFileManager defaultManager]; NSError *outErr; for (NSString *item in [fm contentsOfDirectoryAtPath:pluginsFolder error:&outErr]) { if (![item hasSuffix:@".bundle"]) { continue; } NSString *bundlePath = [pluginsFolder stringByAppendingPathComponent:item]; NSBundle *b = [NSBundle bundleWithPath:bundlePath]; if (!b) { NSLog(@“Could not make a bundle from %@”, bundlePath); continue; } //获取实现插件代理协议方法的类 idplugin = [[b principalClass] new]; NSMenuItem *item = [pluginsMenu addItemWithTitle:[plugin menuItemTitle] action:@selector(pluginMenuItemCalledAction:) keyEquivalent:@""]; [item setRepresentedObject:plugin]; } {% endcodeblock %}
注:扫描插件目录,确保得到的是一个
.bundle
文件,然后用NSBundle
载入你找到的bundle
并且实例化里面的类。
插件代理
你会注意到一个 TextEditPlugin
的 protocol
的引用。在 TextEditMisc.h
能找它的定义:
{% codeblock 声明代理协议 lang:objc %}
@protocol TextEditPlugin
- (NSString*)menuItemTitle;
- (void)actionCalledWithTextView:(NSTextView*)textView inDocument:(id)document; @end {% endcodeblock %} 这说明你实例化的类需要响应这两个方法。你可以验证这个类是否响应这两个方法。
NSPrincipalClass键:值–实现插件代理协议方法的类名称
在 bundle
里面调用的 principalClass
方法是什么呢?
当你创建一个 Bundle
的时候,你可以在里面创建一个或者多个类,同时你需要让 TextEdit
知道哪一个类需要被实例化。为了帮助宿主 App 调用,你可以在 Info.plist
文件加入一个 NSPrincipalClass
的键,同时设置它的值为实现插件方法的类的名字。你可以用 [NSBundle principalClass]
方便地从 NSPrincipalClass
的值里面寻找并创建这个类。
添加扩展插件菜单的响应事件
在 Plug-Ins
菜单加入一个新的按钮,设置 action
为 pluginMenuItemCalledAction:
,并且设置它表示你已经实例化的对象。
如果在
menu item
里面没有设置一个target
,即目标是nil
,那么它会寻找响应链,来寻找第一个实现pluginMenuItemCalledAction:
方法的对象。如果它找不到,那么这个菜单选项将会不能用。 举一个例子,实现pluginMenuItemCalledAction
的最好的地方是在Document
的window controller
类中。打开DocumentWindowController.m
,然后定位到pluginMenuItemCalledAction
{% codeblock lang:objc %}
- (void)pluginMenuItemCalledAction:(id)sender
{
id
plugin = [sender representedObject]; [plugin actionCalledWithTextView:[self firstTextView] inDocument:[self document]]; } {% endcodeblock %}
代码本身很清晰,搜集插件实例,调用 actionCalledWithTextView:inDocument:
方法(被定义在 protocol
里面的),运行你插件里面的代码。
制作插件
- 新建模版项目
打开 “01 MarkYellow” 工程看一下。这是一个 Xcode (通过
OS X ▸ Framework & Library ▸ Bundle template
建立) 的标准工程,里面只添加了一个类:TEMarkYellow
。 - 设置NSPrincipalClass键值
如果你打开
MarkYellow-Info.plist
,你可以看到NSPrincipalClass
的值设置成了上面提到的TEMarkYellow
。 - 实现协议代理
接着,打开
TEMarkYellow.m
,你将会看见定义在协议里面的方法。- 第一个方法(
menuItemTitle
)返回插件的名字,最为入口名显示在menu
里面。 - 第二个方法 (
actionCalledWithTextView:inDocument:
),把所有选中的文字变成黄色的背景。 {% codeblock 改变字体颜色 lang:objc %}
- 第一个方法(
- (void)actionCalledWithTextView:(NSTextView*)textView inDocument:(id)document { if ([textView selectedRange].length) { NSMutableAttributedString *ats = [[[textView textStorage] attributedSubstringFromRange:[textView selectedRange]] mutableCopy]; [ats addAttribute:NSBackgroundColorAttributeName value:[NSColor yellowColor] range:NSMakeRange(0, [ats length])]; // 先测试text view是否能改变文字内容,这样可以自动做正确的撤销操作。 By asking the text view if you can change the text first, it will automatically do the right thing to enable undoing of attribute changes if ([textView shouldChangeTextInRange:[textView selectedRange] replacementString:[ats string]]) { [[textView textStorage] replaceCharactersInRange:[textView selectedRange] withAttributedString:ats]; [textView didChangeText]; } } } {% endcodeblock %}
集成插件
运行 TextEdit
(它会创建Plug-Ins
目录),然后构建 MarkYellow
工程。把 MarkYellow.bundle
丢到你的 ~/Library/Application Support/Text Edit/Plug-Ins/
目录下面,重启你的 TextEdit
应用。
一切看起来都很好,扫描,加载,插入一个菜单,然后,当你使用菜单项的时候,传递到参数到插件里面。试一试,点击 Plug-Ins ▸ Mark Selected Text Yellow
,选择的文字的背景颜色就变成黄色的了。
XCode8版本问题
Xcode8 不再支持Application Plug-in
插件
XCode8.2.1继续使用xcode插件
这个是因为苹果解决xcode ghost,把插件屏蔽了。
解决方法
sudo /usr/libexec/xpccachectl
然后必须重启电脑后生效.
文章作者 iTBoyer
上次更新 2017-02-07