• iOS


    上一篇《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


  • 相关阅读:
    递归函数
    Java以缓冲字符流向文件写入内容(如果文件存在则删除,否则先创建后写入)
    Python将文本内容读取分词并绘制词云图
    查询数据库数据并传入servlet
    向数据库添加记录(若有则不添加)
    2月16日学习记录
    2月15日学习记录
    2月14日学习记录
    Echart学习
    JavaScript深入学习(六)Ajax技术
  • 原文地址:https://www.cnblogs.com/junhuawang/p/8183003.html
Copyright © 2020-2023  润新知