iOS扩展开发攻略Action Extension
Posted 杰嗒嗒的阿杰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS扩展开发攻略Action Extension相关的知识,希望对你有一定的参考价值。
【ios扩展开发攻略】Action Extension
- 1. 创建Action Extension扩展Target
- 2. 分析扩展例子代码
- 3. 改写例子:选中网页名词解释
- 4. 带UI的Action Extension
- 5. 改写例子:获取网页中的所有图片
- 6. 相关文章
上一篇《iOS扩展开发攻略:Share Extension》介绍了分享扩展的开发与使用,本篇主要还是讲述在系统分享菜单中最底下一栏的功能扩展:Action Extension,该扩展跟Share Extension实现比较类似只是在使用场景上进行了区分,Share Extension主要用于将Host应用中的内容分享到Container应用中,而Action Extension则主要用于将Host应用中的内容进行对应处理,原则上来说作用范围比Share Extension要广。
那么,下面将详细讲解开发Action Extension具体的操作步骤:
1. 创建Action Extension扩展Target
1、打开项目设置,在TARGETS侧栏地下点击“+”号来创建一个新的Target,如图:
2、然后选择”iOS” -> “Application Extension” -> “Action Extension”,点击“Next”。如图:
3、给扩展起个名字,这里填写了“Action”,然后要注意Action Type这里有两个选项:** Presents User Interface 和 No User Interface **。前者是触发扩展后会弹出一个UI界面,后者是不带界面的扩展。这里我会分两部分进行讲解,先从无UI的扩展开始,所以选择了No User Interface,点解Finish完成创建。如图:
4、这时候会提示创建一个Scheme,点击“Activate”。如图:
一个无UI的Action Extension Target到此已经创建完成了。下面先来看一下新建的扩展结构,如下图所示:
扩展的文件组织结构描述如下:
文件 | 说明 |
---|---|
ActionRequestHandler.h | 扩展处理类的头文件,对处理类型的声明描述。 |
ActionRequestHandler.m | 扩展处理类的实现文件,处理扩展实际的业务逻辑。 |
Action.js | 与Web也进行交互的脚本,后续会详细介绍它的作用。 |
Info.plist | 扩展的配置文件 |
先Command+R编译运行默认的扩展来看一下实际效果。
可以看到在弹出的分享菜单的底下一栏多了一个叫Action的小图标(演示图1),并且点击后网页的背景颜色变成红色(演示图2)。下面将对这个例子进行详细的讲解。
2. 分析扩展例子代码
先打开ActionRequestHandler.h头文件,可以看到扩展的处理类ActionRequestHandler
的定义,代码如下:
@interface ActionRequestHandler : NSObject <NSExtensionRequestHandling>
@end
上面的类型实现了一个NSExtensionRequestHandling
的协议。这也是无UI的扩展对象必须要实现的协议,否则无法向处理类返回正确的回调。我们可以看一下协议的声明:
@protocol NSExtensionRequestHandling <NSObject>
@required
- (void)beginRequestWithExtensionContext:(NSExtensionContext*)context;
@end
协议只有一个方法beginRequestWithExtensionContext:
,就是点击扩展图标的时候就会触发这个方法,并将扩展的上下文作为参数进行回调(关于NSExtensionContext相关内容在《iOS扩展开发攻略:Share Extension》有讲述)。所以无UI的扩展相对来说比较简单,只要实现这个方法的处理即可。下面就来看一下例子中的.m文件是怎么处理的。
- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context
// Do not call super in an Action extension with no user interface
self.extensionContext = context;
BOOL found = NO;
// Find the item containing the results from the javascript preprocessing.
for (NSExtensionItem *item in self.extensionContext.inputItems)
for (NSItemProvider *itemProvider in item.attachments)
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypePropertyList])
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *dictionary, NSError *error)
[[NSOperationQueue mainQueue] addOperationWithBlock:^
[self itemLoadCompletedWithPreprocessingResults:dictionary[NSExtensionJavaScriptPreprocessingResultsKey]];
];
];
found = YES;
break;
if (found)
break;
if (!found)
// We did not find anything
[self doneWithResults:nil];
从上面代码可知,扩展是通过匹配上下文(NSExtensionContext
)的inputItem的附件(attachment)类型是否为PropertyList。然后再通过loadItemForTypeIdentifier
方法加载附件后进行相应的处理(关于NSExtensionItem相关内容在《iOS扩展开发攻略:Share Extension》有讲述)。其中处理方法itemLoadCompletedWithPreprocessingResults
代码如下:
- (void)itemLoadCompletedWithPreprocessingResults:(NSDictionary *)javaScriptPreprocessingResults
if ([javaScriptPreprocessingResults[@"currentBackgroundColor"] length] == 0)
// No specific background color? Request setting the background to red.
[self doneWithResults:@ @"newBackgroundColor": @"red" ];
else
// Specific background color is set? Request replacing it with green.
[self doneWithResults:@ @"newBackgroundColor": @"green" ];
- (void)doneWithResults:(NSDictionary *)resultsForJavaScriptFinalize
if (resultsForJavaScriptFinalize)
// Construct an NSExtensionItem of the appropriate type to return our
// results dictionary in.
// These will be used as the arguments to the JavaScript finalize()
// method.
NSDictionary *resultsDictionary = @ NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize ;
NSItemProvider *resultsProvider = [[NSItemProvider alloc] initWithItem:resultsDictionary typeIdentifier:(NSString *)kUTTypePropertyList];
NSExtensionItem *resultsItem = [[NSExtensionItem alloc] init];
resultsItem.attachments = @[resultsProvider];
// Signal that we're complete, returning our results.
[self.extensionContext completeRequestReturningItems:@[resultsItem] completionHandler:nil];
else
// We still need to signal that we're done even if we have nothing to
// pass back.
[self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
// Don't hold on to this after we finished with it.
self.extensionContext = nil;
从代码可以看到itemLoadCompletedWithPreprocessingResults
简单地判断字典对象的currentBackgroundColor键值是否有存在背景颜色,如果不存在任何背景颜色,则返回一个红色作为新背景颜色,如果存在背景颜色,则返回一个绿色作为新的背景颜色,然后以字典方式传给doneWithResults方法。
而doneWithResults
方法使这个新背景颜色字典包含在另一个字典的NSExtensionJavaScriptFinalizeArgumentKey
键中并使用NSItemProvider包装。最后构建NSExtensionItem对象并使用上下文的completeRequestReturningItems
方法进行返回,并告知系统扩展的操作结束。
2.1 与Safari中的网页进行交互
在整个处理中我们并没有发现扩展有对网页的背景颜色进行设置。是怎么做到调整网页的样式的呢?重点就是在于Action.js这个JS文件中,打开Action.js:
var Action = function() ;
Action.prototype =
run: function(arguments)
arguments.completionFunction( "currentBackgroundColor" : document.body.style.backgroundColor )
,
finalize: function(arguments)
var newBackgroundColor = arguments["newBackgroundColor"]
if (newBackgroundColor)
// We'll set document.body.style.background, to override any
// existing background.
document.body.style.background = newBackgroundColor
else
// If nothing's been returned to us, we'll set the background to
// blue.
document.body.style.background= "blue"
;
var ExtensionPreprocessingJS = new Action
可以看到JS文件中有一个Action的类型定义,其中run
和finalize
两个方法方法。
-
run
方法
在扩展激活后调用NSItemProvider
的loadItemForTypeIdentifier
方法时被调用**(注:此时加载的Type为kUTTypePropertyList,因为一旦设置JS文件则能够检测到该类型的NSItemProvider
)**,通过该方法的arguments参数的completionFunction
方法可以给原生层传入一个数据对象。 -
finalize
方法
该方法的调用时机在扩展原生层调用completeRequestReturningItems
后触发,这里有一个必要的触发条件,就是必须要扩展返回一个带有NSExtensionJavaScriptFinalizeArgumentKey
的ExtensionItem,否则finalize
方法不会执行。该方法能够通过arguments
参数获取原生层返回的ExtensionItem包含在NSExtensionJavaScriptFinalizeArgumentKey
中的内容。
上面的例子可以看到在扩展激活后,加载PropertyList类型的附件时JS会执行run
方法,并把当前背景颜色传入给原生层。然后等待原生层处理完成后在finialize
方法中捕获原生层返回的新背景颜色值并进行设置。综合上所述,可以知道扩展的执行过程如下面流程图所示**(PS. 经过跟同事讨论后发现自己之前的理解有所偏差,现在执行过程流程图作出一些调整,同时感谢提出问题的同事们_)**:
2.2 为扩展配置JS文件
了解了JS文件的工作原理后,下面给大家讲解一下如何给Action Extension配置一个JS处理文件:
-
创建一个JS文件,如例子中的Action.js。
-
在JS文件中创建一个JS类型,这个类型必须要有run和finalize方法,用作系统对JS的回调。
-
打开Info.plist文件,在NSExtension -> NSExtensionAttributes下创建一项NSExtensionJavaScriptPreprocessingFile,然后将将JS文件的名字写入该项。如图所示:
完成上面步骤后即可与网页的js代码进行交互了。(** 注:NSExtensionJavaScriptPreprocessingFile在Share Extension中同样适用 **)。
3. 改写例子:选中网页名词解释
下面我们来改写一下自带的例子,让扩展可以知道我们选中了网页的哪些内容,然后给内容进行一个解释。目的是让大家了解建立一个Action Extension需要什么步骤。
首先创建一个新的处理类型ExplainActionRequestHandler,并实现NSExtensionRequestHandling
协议。如:
@interface ExplainActionRequestHandler : NSObject <NSExtensionRequestHandling>
@end
然后创建一个新的JS脚本ExplainAction.js,写上初始化的定义。如:
var ExplainAction = function() ;
ExplainAction.prototype =
run: function(arguments)
,
finalize: function(arguments)
;
var ExtensionPreprocessingJS = new ExplainAction
然后打开Info.plist来对扩展进行配置,进行下面几项设置:
- 定位到NSExtension -> NSExtensionAttributes -> NSExtensionActivationRule,调整扩展的匹配规则。之前的规则都删除掉,然后添加NSExtensionActivationSupportsWebPageWithMaxCount这个Key,并设置其值为1。
- 把NSExtension -> NSExtensionAttributes -> NSExtensionJavaScriptPreprocessingFile 设置为 ExplainAction
- 把NSExtension -> NSExtensionPrincipalClass 设置为 ExplainActionRequestHandler
如图所示:
然后,在ExplainAction.js文件中实现JS层获取选中文本,可以根据window.getSelection()
方法来取得。如:
run: function(arguments)
arguments.completionFunction( "text" : window.getSelection().toString() );
,
接着,回到ExplainActionRequestHandler
的类实现,处理NSExtensionRequestHandling
协议的beginRequestWithExtensionContext
方法,如:
- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context
__weak typeof(self) weakSelf = self;
NSExtensionItem *item = context.inputItems.firstObject;
NSItemProvider *itemProvider = item.attachments.firstObject;
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypePropertyList])
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList
options:nil
completionHandler:^(id<NSSecureCoding> _Nullable item, NSError * _Null_unspecified error)
NSDictionary *jsData = item[NSExtensionJavaScriptPreprocessingResultsKey];
NSString *text = jsData[@"text"];
if (text)
//进行文本解释
[weakSelf resultExplainWithData:@@"explain" : @"问我之前请先百度一下", @"text" : text context:context];
else
[context completeRequestReturningItems:nil completionHandler:nil];
];
代码基本与例子中的处理类似,主要是找到PropertyList类型的附件,然后从附件中取得JS传递过来的数据,然后根据数据进行一个解释处理,最后返回一个带有解释字段(explain)的字典到JS。最后JS层将内容输出,如:
finalize: function(arguments)
alert(arguments["text"] + ":" + arguments["explain"]);
Command+R运行扩展程序,先选中一段文字,然后再点击Safari工具栏的分享按钮,点击Action图标就能够看到弹出一个对文本进行解释的对话框了。如图:
** 注:如果直接在选中文件时弹出的菜单中点击分享时无法出发JS脚本的,只有点击Safari工具栏的分享按钮才能够触发JS脚本,这也算是这个功能的一个局限。**
4. 带UI的Action Extension
上面已经对无UI扩展进行了详细的描述,接下来我们继续讲述带UI的扩展相关的一些内容,以及它跟无UI扩展的一些区别。
为了方便对比,我们再新建一个带UI的Action Extension Target,具体步骤与无UI的一样,只是扩展配置中选择“Presents User Interface”,完成后可以看到新建的扩展Target,如下图所示:
扩展的文件组织结构描述如下:
文件 | 说明 |
---|---|
ActionViewController.h | 扩展视图控制器的头文件,激活扩展后弹出的视图类型声明。 |
ActionViewController.m | 扩展视图控制器的实现文件,处理扩展视图的业务逻辑。 |
MainInterface.storyboard | UI的布局与流程描述文件。 |
Info.plist | 扩展的配置文件 |
下面是我整理不同Action Type的对比
Presents User Interface | No User Interface |
---|---|
带有一个ViewController的子类,用于显示和处理扩展中相关信息。 | 带有一个NSObject的子类,需要实现NSExtensionRequestHandling协议,用于扩展的相关处理。 |
Info.plist文件中的NSExtensionPointIdentifier为com.apple.ui-services | Info.plist文件中的NSExtensionPointIdentifier为com.apple.services |
Info.plist文件中可以指定NSExtensionMainStoryboard或者NSExtensionPrincipalClass来设置扩展的视图 | Info.plist文件中只能够通过指定NSExtensionPrincipalClass来设置扩展的处理类型 |
保留默认的处理逻辑,Command+R运行扩展来观察效果。这次设置的Host App为相册,因为默认的处理是在UI中显示处理的图片。其运行效果如下:
带UI的扩展大体实现代码跟无UI的类似,因为扩展需要弹出一个UI界面,因此一些扩展的初始化逻辑会放入到viewDidLoad
方法中执行。如:
- (void)viewDidLoad
[super viewDidLoad];
// Get the item[s] we're handling from the extension context.
// For example, look for an image and place it into an image view.
// Replace this with something appropriate for the type[s] your extension supports.
BOOL imageFound = NO;
for (NSExtensionItem *item in self.extensionContext.inputItems)
for (NSItemProvider *itemProvider in item.attachments)
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage])
// This is an image. We'll load it, then place it in our image view.
__weak UIImageView *imageView = self.imageView;
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *image, NSError *error)
if(image)
[[NSOperationQueue mainQueue] addOperationWithBlock:^
[imageView setImage:image];
];
];
imageFound = YES;
break;
if (imageFound)
// We only handle one image, so stop looking for more.
break;
主要也是判断NSExtensionItem的附件中是否包含图片类型,如果存在则显示到视图中。
5. 改写例子:获取网页中的所有图片
接下来我们对这个扩展进行改写,让它能够跑在Safari上并且能够解析打开网页的所有图片。既然是要解析网页那么就需要使用JS文件来配合扩展的工作。
首先我们创建一个Action.js文件,并定义好其结构框架,如:
var Action = function() ;
Action.prototype =
run: function(arguments)
,
finalize: function(arguments)
;
var ExtensionPreprocessingJS = new Action
然后创建一个新的视图控制器ImageListViewController
,其继承于UITableViewController
。如:
@interface ImageListViewController : UITableViewController
@end
然后打开Info.plist文件,将新建的JS文件和ImageListViewController视图控制器配置进来,调整后如下图所示:
接着,我们要实现从网页中获取图片对象,具体思路是通过document.getElementsByTagName
方法获取网页中的img标签,然后把img标签的src属性取出来传给原生层。代码如下:
run: function(arguments)
var imgs = document.getElementsByTagName("img");
var imgUrls = [];
for (var i = 0; i < imgs.length; i++)
if (imgs[i].src != null && imgs[i].src.indexOf("http") == 0)
imgUrls.push(imgs[i].src);
arguments.completionFunction("imgs" : imgUrls);
,
上面的代码对img的src属性进行了筛选,排除了为空并且不以http开头的图片地址。然后回到ImageListViewController
中对传入参数进行解析,并刷新tableView。代码如下:
- (void)viewDidLoad
[super viewDidLoad];
self.tableView.rowHeight = 100;
//解析JS传递过来的数据
NSExtensionItem *item = self.extensionContext.inputItems.firstObject;
NSItemProvider *itemProvider = item.attachments.firstObject;
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypePropertyList])
__weak typeof(self) weakSelf = self;
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList
options:nil
completionHandler:^(id<NSSecureCoding> _Nullable item, NSError * _Null_unspecified error)
//找到JS返回数据
NSDictionary *jsData = item[NSExtensionJavaScriptPreprocessingResultsKey];
NSArray *imgUrls = jsData[@"imgs"];
dispatch_async(dispatch_get_main_queue(), ^
//设置数据源,刷新表格
weakSelf.imgUrls = imgUrls;
[weakSelf.tableView reloadData];
);
];
//创建一个关闭按钮
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.backgroundColor = [UIColor blueColor];
[btn setTitle:@"Close" forState:UIControlStateNormal];
btn.frame = CGRectMake(0, 0, self.tableView.frame.size.width, 50);
btn.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
[btn addTarget:self action:@selector(closeButtonClickedHandler:) forControlEvents:UIControlEventTouchUpInside];
self.tableView.tableHeaderView = btn;
cell的数据填充渲染就不细说了,有需要的同学可以查看源码,最后Command+R运行扩展,设置Host App为Safari,然后打开一个图片网站,激活扩展,可以得到下面的效果:
6. 相关文章
以上是关于iOS扩展开发攻略Action Extension的主要内容,如果未能解决你的问题,请参考以下文章