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的類型定義,其中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,如下圖所示:
創建帶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 運行效果圖2iOS開發 之 Action Extension