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,如图:


 
创建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”。如图:

 
创建Scheme

一个无UI的Action Extension Target到此已经创建完成了。下面先来看一下新建的扩展结构,如下图所示:

 
扩展的文件组织结构

扩展的文件组织结构描述如下:

文件说明
ActionRequestHandler.h 扩展处理类的头文件,对处理类型的声明描述。
ActionRequestHandler.m 扩展处理类的实现文件,处理扩展实际的业务逻辑。
Action.js 与Web也进行交互的脚本,后续会详细介绍它的作用。
Info.plist 扩展的配置文件

先Command+R编译运行默认的扩展来看一下实际效果。

 
演示效果图1
 
演示效果图2

可以看到在弹出的分享菜单的底下一栏多了一个叫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的类型定义,其中runfinalize两个方法方法。

  • run方法
    在扩展激活后调用NSItemProviderloadItemForTypeIdentifier方法时被调用(注:此时加载的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处理文件:

  1. 创建一个JS文件,如例子中的Action.js。

  2. 在JS文件中创建一个JS类型,这个类型必须要有run和finalize方法,用作系统对JS的回调。

  3. 打开Info.plist文件,在NSExtension -> NSExtensionAttributes下创建一项NSExtensionJavaScriptPreprocessingFile,然后将将JS文件的名字写入该项。如图所示:

 
设置预加载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,如下图所示:

 
创建带UI的扩展Target

扩展的文件组织结构描述如下:

文件说明
ActionViewController.h 扩展视图控制器的头文件,激活扩展后弹出的视图类型声明。
ActionViewController.m 扩展视图控制器的实现文件,处理扩展视图的业务逻辑。
MainInterface.storyboard UI的布局与流程描述文件。
Info.plist 扩展的配置文件

下面是我整理不同Action Type的对比

Presents User InterfaceNo 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中显示处理的图片。其运行效果如下:

 
运行效果图1
 
运行效果图2

带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,然后打开一个图片网站,激活扩展,可以得到下面的效果:

 
运行效果图1
 
运行效果图2


posted @ 2018-01-03 11:35  俊华的博客  阅读(...)  评论(...编辑  收藏