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

运行效果图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



原文地址:https://www.cnblogs.com/junhuawang/p/8183003.html

时间: 2024-10-29 09:36:09

iOS开发 之 Action Extension的相关文章

iOS开发------Widget(Today Extension)插件化开发

iOS10.0发布啦(貌似过去有点时间了吧 - -),在宏观带给我们使用体验的提升之外,更多的是带给iOS开发者一定的欣喜. 因为我们又要学习新东西来适配10啦. 博文所说的Widget(以下称之为拓展应用)并不是iOS10系统新推出的插件化应用(其实早在iOS8上就已经出现啦,只不过楼主是在iOS10发布之后才算真正的关注它,实在是惭愧呀).iOS10之前它仅仅是存在于通知那一栏中,至于多隐蔽我就不说了吧.但在iOS10之后获得重生,地位获得了巨大的提升,从这点也不难看出苹果增加了对它的重视.

iOS开发日记55-通知栏扩展(App Extension)

今天博主有一个App Extension的需求,遇到了一些困难点,在此和大家分享,希望能够共同进步. 总览 扩展 (Extension) 是 iOS 8 和 OSX 10.10 加入的一个非常大的功能点,开发者可以通过系统提供给我们的扩展接入点 (Extension point) 来为系统特定的服务提供某些附加的功能.对于 iOS 来说,可以使用的扩展接入点有以下几个: Today 扩展 - 在下拉的通知中心的 "今天" 的面板中添加一个 widget 分享扩展 - 点击分享按钮后将网

iOS开发OC基础:延展Extension

//延展Extension,是为类扩充私有的方法,以及私有的实例变量,和分类Category相比,延展定义的方法是私有的,而且还可以定义实例变量(私有的). //@interface 开头 + 类名(哪一个类的延展) + 小括号,小括号内填写的是延展名,一般情况下都省略. @end结束 (写在.m文件里) main函数中的完整代码为: #import <Foundation/Foundation.h> #import "Person.h" int main(int argc

从零开始学ios开发(七):Delegate,Action Sheet, Alert

Action Sheet和Alert是2种特殊的控件(暂且称之为控件吧,其实不是控件真正的控件,而是ios中的2个类,这2个类定义了2种不同类型的用于和用户交互的弹出框),Action Sheet是从底部弹出,上面有2个或者2个以上的选项供用户选择,Alert就是一个警告框,上面有1个或者1个以上的按钮供用户进行选择. 在继续这一篇的内容之前,稍微花点时间说一下ios中用到的Delegate Pattern(委托\代理模式). ios中有很多已经定义好的类可以供我们在编写程序时直接使用,例如UI

iOS开发——OC篇&amp;消息传递机制(KVO/NOtification/Block/代理/Target-Action)

iOS开发中消息传递机制(KVO/NOtification/Block/代理/Target-Action) 今晚看到了一篇好的文章,所以就搬过来了,方便自己以后学习 虽然这一期的主题是关于Foundation Framework的,不过本文中还介绍了一些超出Foundation Framework(KVO和Notification)范围的一些消息传递机制,另外还介绍了delegation,block和target- action. 大多数情况下,消息传递该使用什么机制,是很明确的了,当然了,在某

iOS开发——面试篇&amp;OC基本语法总结(面试)

OC基本语法总结(面试) C和OC对比 OC中主要开发在什么平台上的应用程序? 答:可以使用OC开发Mac OS X平台和iOS平台的应用程序 OC中新增关键字大部分是以什么开头? 答:OC中新增关键字大部分是以@开头 OC中新增加了那些数据类型? 答: Block类型 指针类型(Class, id类型) 空类型 特殊类型(SEL, nil) 面向对象特性是什么? 答:继承性,封装性,多态性 import和#include有什么区别? 答:import 的功能和 include一样, 是将右边的

iOS开发项目篇—27自定义UITabBar

iOS开发项目篇—27自定义UITabBar 一.自定义 思路: (1)新建一个继承自UITabBar的类,自定义一个UITabBar (2)用自定义的UITabBar换掉系统的UItabBar(使用了KVC) (3)监听控制器的切换,只要控制器一切换,就调用代理方法强制重新布局子控件(内部会调用layoutSubviews). YYTabBar.m文件代码: 1 // 2 // YYTabBar.m 3 // 4 5 #import "YYTabBar.h" 6 7 @interfa

iOS开发项目篇—39获取用户未读的微博信息(信息提醒)

iOS开发项目篇—39获取用户未读的微博信息(信息提醒) 一.简单说明 1.实现效果       2.实现 (1)新建一个类,封装请求 查看新浪官方要求的请求参数 该类中的代码设计 YYUnreadCountParam.h文件 1 // YYUnreadCountParam.h 2 //封装请求参数的类 3 4 #import "YYBaseParam.h" 5 6 @interface YYUnreadCountParam : YYBaseParam 7 /**uid true in

iOS开发技巧系列---使用链式编程和Block来实现UIAlertView

UIAlertView是iOS开发过程中最常用的控件之一,是提醒用户做出选择最主要的工具.在iOS8及后来的系统中,苹果更推荐使用UIAlertController来代替UIAlertView.所以本文也并不提倡开发者再使用UIAlertView,本文的目的是探讨如何将原来的给变量赋值和通过Delete来回调的方式变成链式编程风格和通过Block来回调.通过学习对UIAlertView的改造让各位iOS开发者能够学会这种更加便捷的开发方式 什么是链式编程 对于有一定开发经验的开发者来说,链式编程