iOS开发 之 Action Extension
Posted 俊华的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发 之 Action Extension相关的知识,希望对你有一定的参考价值。
上一篇《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,然后打开一个图片网站,激活扩展,可以得到下面的效果:
以上是关于iOS开发 之 Action Extension的主要内容,如果未能解决你的问题,请参考以下文章
iOS In App Purchase in Action Extension 应用程序
iOS开发之缓存框架内存缓存磁盘缓存NSCacheTMMemoryCachePINMemoryCacheYYMemoryCacheTMDiskCachePINDiskCache(示例代