1. 程式人生 > >iOS開發 之 Action Extension

iOS開發 之 Action Extension

lec afa 彈出 int -h prop eight 配置文件 blue

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


iOS開發 之 Action Extension