本節(jié)由CocoaChina翻譯組成員DevTalking (博客 )翻譯自蘋果官方文檔 App Extension Programming Guide--Handling Common Scenarios 一節(jié),,敬請勘誤,。歡迎加入我們的翻譯小組,詳情請參看:CocoaChina編輯和譯者招募,!
當編寫自定義代碼以執(zhí)行app擴展任務時,,你可能需要處理一些其他多種類型擴展也會出現(xiàn)的情況。在這一章節(jié)中,,我們將幫助你如何應對和處理這些常見的問題,。
使用內嵌框架共享代碼
你可以創(chuàng)建一個內嵌框架,用于在應用擴展和它的主應用程序(containing app)之間共享代碼,。比如,,你在照片編輯擴展中開發(fā)了圖片濾鏡功能,那么同時該擴展的containing app也有這個功能,,那么你可以將實現(xiàn)該功能的代碼封裝成一個框架,,并在擴展target和主應用程序target中嵌入這個框架。
你要確保你創(chuàng)建的內嵌框架不包含應用擴展不能使用的API。這類API一般使用unavailability宏來標記,,比如像 NS_EXTENSION_UNAVAILABLE,。
如果你創(chuàng)建的內嵌框架中包含應用擴展不能使用的API,你可將其安全地Link到containing app,,它可以正常使用框架中的API,,但是不能與應用擴展共享代碼(譯者注:也就是應用擴展不能使用該框架提供的所有API,繼而無法做到代碼共享),。如果你上傳App Store的應用擴展中有這種框架,,或者其他部分使用了不可用的API,那么審核時會被拒絕,。
如果我們要想應用擴展使用內嵌框架,,那么首先要配置一下。將target的Require Only App-Extension-Safe API選項設置為Yes,。如果你不這樣設置,,那么Xcode會向你提示警告:linking against dylib not safe for use in application extensions。
重要提示:如果containing app要鏈接至內嵌框架,,那么必須要支持arm64架構,,否則在上傳App Store時會被拒絕。(如“創(chuàng)建應用擴展”章節(jié)中介紹的,,所有應用擴展都要支持arm64架構,。)
在配置你的 Xcode 項目時,在 Build Phases 選項卡的 Copy Files 項中一定要將 Destination 設置為 Frameworks,。
重要提示:我們通常要選擇 Frameworks 作為 Copy Files build phase 目的地,。如果你將其設置為 SharedFramework,那么上傳App Store時會被拒絕的,。
你可以讓containing app支持iOS7或更早的版本,,但當在iOS8或更新的版本中運行時,要特別注意內嵌框架的安全性,。詳細內容可以參閱 Deploying a Containing App to Older Versions of iOS,。
有關創(chuàng)建和使用內嵌框架的更多內容,請觀看WWDC 2014的視頻“Building Modern Frameworks”,。
與Containing App共享數(shù)據(jù)
應用擴展和它的containing app的安全域是有區(qū)別的,。即便擴展包是嵌套在containing app包中的。默認情況下,,應用擴展和containing app是不能直接訪問對方的容器的,。
不過你可以通過數(shù)據(jù)共享來實現(xiàn)這個愿望,。比如,,你希望應用擴展和它的containing app共享一個單一的大數(shù)據(jù)集。比如prerendered assets。
要實現(xiàn)數(shù)據(jù)共享,,我們要使用Xcode或者開發(fā)者門戶網站允許應用擴展和它的containing app成為一個應用組,,然后在開發(fā)者門戶網站中注冊應用組,并指明在containing app中使用該應用組,。關于應用組的知識請查閱 Entitlement Key Reference 文檔的 Adding an App to an App Group 章節(jié),。
當你設置好應用組后,應用擴展和它的containing app就可以通過 NSUserDefaults API共享訪問用戶的信息,。我們可以使用 initWithSuiteName: 方法實例化一個 NSUserDefaults 對象,,然后傳入共享組的標示符。比如一個共享擴展,,它或許會更新用戶最近經常使用的共享賬號,,那么我們可以這樣來寫:
- // Create and share access to an NSUserDefaults object.
- NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.example.domain.MyShareExtension"];
-
-
- // Use the shared user defaults object to update the user's account.
- [mySharedDefaults setObject:theAccountName forKey:@"lastAccountName"];
下圖向我們展示了應用擴展和它的containing app是如何通過共享容器實現(xiàn)數(shù)據(jù)共享的.
Figure 4-1應用擴展的容器與其containing app的容器是不同的。
如果你設置了共享容器,,那么containing app和它包含的允許參與數(shù)據(jù)分享的擴展就可以對共享容器里的內容進行讀寫操作了。同時你還必須要對數(shù)據(jù)的操作進行同步,,以避免數(shù)據(jù)損壞或出錯,。使用UIDocument類、Core Data或者SQLite可以幫你可以讓用戶通過要求Safari運行JS文件來訪問網絡內容,,并將結果返回到擴展,。
訪問網頁
在分享擴展(iOS與OS X平臺)和Action擴展(iOS平臺)中,一般都允許用戶使用Safari瀏覽器訪問網頁并通過執(zhí)行JavaScript腳本,,并將結果返回到擴展中,。你也可以在你的擴展運行之前(適用于兩個平臺)或執(zhí)行完任務之后(僅適用于iOS平臺)通過JavaScript文件修改網頁內容。比如分享擴展,,它可以幫助用戶分享網頁上的內容,,或者iOS上的Action擴展可能會顯示當前網頁的指定翻譯內容。
如果想添加網頁訪問和操作應用擴展,,那么需要遵循下面幾個步驟:
1.創(chuàng)建一個JavaScript文件,,并申明一個全局對象,命名為 ExtensionPreprocessingJS,,并為該對象分配一個新的自定義JavaScript類的實例,。
2.在應用擴展的屬性列表文件中添加關鍵字 NSExtensionJavaScriptPreprocessingFile,給 Safari 瀏覽器指明使用哪個 JavaScript 文件,。
3.在NSExtensionActivationRule字典中,,將 NSExtensionActivationSupportsWebURLWithMaxCount 賦值一個非零的值,。(更多關于 NSExtensionActivationRule 字典的知識請參閱 Declaring Supported Data Types for a Share or Action Extension。)
4.當你的應用擴展開始運行時,,使用NSItemProvider類獲得運行JavaScript文件所返回的結果,。
5.在iOS系統(tǒng)的應用擴展中,如果你希望Safari在擴展執(zhí)行完任務后更新網頁,,那么你要向JavaScript文件中傳入值,。(在這一步中也使用NSItemProvider類。)
為了告知Safari你的應用擴展中包含一個JavaScript文件,,你需要在應用擴展的Info.plist文件中,,向NSExtensionAttributes字典添加NSExtensionJavaScriptPreprocessingFile關鍵字來指明你的JavaScript文件。這個關鍵字的值就是你希望當你的應用擴展運行前,,Safari要加載的JavaScript文件的名稱,。比如:
- NSExtensionAttributes
-
- NSExtensionJavaScriptPreprocessingFile
- MyJavaScriptFile
-
在iOS和OS X平臺中,在你自定義的JavaScript類中可以定義一個run()函數(shù),,該函數(shù)就是Safari加載JavaScript文件的入口,。在run()函數(shù)中,Safari提供了一個名為completionFunction的參數(shù),,你可以使用鍵值對象的形式將結果傳給應用擴展,。
在iOS平臺中,你還可以定義一個finalize()函數(shù),,當應用擴展在任務結束階段調用completeRequestReturningItems:expirationHandler:completion:方法時Safari會調用finalize()函數(shù),。在該函數(shù)中,可以通過向completeRequestReturningItems:expirationHandler:completion:方法傳值,,來改變網頁內容,。
比如,你的iOS應用擴展需要基于一個網頁URI啟動,,并且當它結束運行時改變網頁的背景色,,那么你需要這樣寫JavaScript代碼:
- var MyExtensionJavaScriptClass = function() {};
-
- MyExtensionJavaScriptClass.prototype = {
- run: function(arguments) {
- // Pass the baseURI of the webpage to the extension.
- arguments.completionFunction({"baseURI": document.baseURI});
- },
-
- // Note that the finalize function is only available in iOS.
- finalize: function(arguments) {
- // arguments contains the value the extension provides in [NSExtensionContext completeRequestReturningItems:expirationHandler:completion:].
- // In this example, the extension provides a color as a returning item.
- document.body.style.backgroundColor = arguments["bgColor"];
- }
- };
-
- // The JavaScript file must contain a global object named "ExtensionPreprocessingJS".
- var ExtensionPreprocessingJS = new MyExtensionJavaScriptClass;
在iOS和OS X平臺中,你需要編寫代碼來處理fun()函數(shù)返回的值,,為獲取到字典中的值,,我們需要指定kUTTypePropertyList類型作為標示符傳入NSItemProvider類的 loadItemForTypeIdentifier:options:completionHandler: 方法。在該字典中使用 NSExtensionJavaScriptPreprocessingResultsKey 作為key來取值,。比如下面例子中我們想要獲取將 URI 傳入 run() 的返回值:
- [imageProvider loadItemForTypeIdentifier:kUTTypePropertyList options:nil completionHandler:^(NSDictionary *item, NSError *error) {
- NSDictionary *results = (NSDictionary *)item;
- NSString *baseURI = [[results objectForKey:NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:@"baseURI"];
- }];
finalize() 函數(shù)是在當應用擴展執(zhí)行完任務后傳參并調用的,,創(chuàng)建一個含有我們需要處理的值的字典,然后用 NSItemProvider 的 initWithItem:typeIdentifier: 方法來封裝該字典,。比如當擴展執(zhí)行完任務后我們想讓網頁變?yōu)榧t色,,我們可以這樣寫:
- NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
- extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"bgColor":@"red"}} typeIdentifier:(NSString *)kUTTypePropertyList]];
- [[self extensionContext] completeRequestReturningItems:@[extensionItem] expirationHandler:nil completion:nil];
執(zhí)行上傳下載任務
用戶一般的操作習慣都傾向于當使用你的應用擴展完成某個任務后,可以將結果立即反饋在使用擴展的應用中,。如果一個擴展要處理的任務包含較長時間的上傳下載操作時,,你要確保當你的應用擴展關閉后能繼續(xù)完成該任務,。為實現(xiàn)這個功能,我們需要使用NSURLSession類創(chuàng)建一個URL會話并創(chuàng)建后臺的上傳下載任務,。
提示:你可以回想一下其他類型的后臺任務,比如后臺支持VoIP,、后臺播放音樂,,這些是不能用應用擴展去實現(xiàn)的。更多信息請參閱Respond to the Host App’s Request,。
當你的應用擴展準備好上傳下載任務后,,擴展會完成調用它的應用發(fā)出的請求,并在不影響上傳下載任務的前提下終止擴展,。更多關于擴展處理主叫應用請求的知識請參閱Respond to the Host App’s Request,。在iOS系統(tǒng)中,如果你的應用擴展在執(zhí)行完后臺任務時并沒有在運行,,那么系統(tǒng)會自動在后臺運行擴展的載體應用,,并調用application:handleEventsForBackgroundURLSession:completionHandler: 代理方法。
重要提示:如果你的應用擴展在后臺創(chuàng)建了 NSURLSession 任務,,那么你必須要設置一個共享容器,,以確保擴展和載體應用實現(xiàn)數(shù)據(jù)共享。我們可以在 NSURLSessionConfiguration 類中使用sharedContainerIdentifier屬性來指定一個共享容器的標示符,,然后我們就可以通過該標示符獲取到共享容器,。請參閱 Sharing Data with Your Containing App 文檔來設置共享容器。
下面的例子展示了如何配置一個URL會話,,并創(chuàng)建一個下載任務:
- NSURLSession *mySession = [self configureMySession];
- NSURL *url = [NSURL URLWithString:@"http://www./LargeFile.zip"];
- NSURLSessionTask *myTask = [mySession downloadTaskWithURL:url];
- [myTask resume];
-
-
- - (NSURLSession *) configureMySession {
- if (!mySession) {
- NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@“com.mycompany.myapp.backgroundsession”];
- // To access the shared container you set up, use the sharedContainerIdentifier property on your configuration object.
- config.sharedContainerIdentifier = @“com.mycompany.myappgroupidentifier”;
- mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
- }
- return mySession;
- }
因為在單位時間內只能由一個進程使用后臺會話,,所以你需要為載體應用中的所有擴展創(chuàng)建不同的后臺會話(每個后臺會話都要有一個唯一的標示符)。在這里我們建議當載體應用在后臺處理擴展的任務時,,只使用一個該擴展創(chuàng)建的后臺會話,。如果你要執(zhí)行其他的網絡相關的任務,那么就要創(chuàng)建相應的URL會話,。
如果你需要在后臺創(chuàng)建URL會話之前完成主叫應用的請求,,那么要確保創(chuàng)建和使用會話的代碼是有效可執(zhí)行的。當你的擴展調用 completeRequestReturningItems:completionHandler: 方法告知主叫應用已經完成相關請求后,,系統(tǒng)就可以隨時終止你的應用擴展,。
為分享和Action擴展申明支持的數(shù)據(jù)類型
在你的分享或Action擴展中,在它們的工作中可能會使用到一些數(shù)據(jù),,并且這些數(shù)據(jù)的類型各不相同,。為了確保只有當用戶在主叫應用中選擇了你的擴展支持的數(shù)據(jù)類型時,才會展示你的擴展功能,。你需要在擴展的屬性列表文件中添加 NSExtensionActivationRule 關鍵字,。你也可以使用該關鍵字指定擴展處理每種類型的最大數(shù)目,。當你的應用擴展運行時,系統(tǒng)會用改關鍵字的值與擴展數(shù)據(jù)項的attachments屬性值作比較,。關于 NSExtensionActivationRule 關鍵字的詳細信息可以參閱 Action Extension Keys 文檔中的 Information Property List Key Reference 章節(jié),。
比如,你可以申明你的分享擴展支持最大處理10張圖片,,一部影片和一個網站URL,。你可以參考下面的寫法:
- NSExtensionAttributes
-
- NSExtensionActivationRule
-
- NSExtensionActivationSupportsImageWithMaxCount
- 10
- NSExtensionActivationSupportsMovieWithMaxCount
- 1
- NSExtensionActivationSupportsWebURLWithMaxCount
- 1
-
-
如果你想指定不支持的數(shù)據(jù)類型,那么你可以將該類型的值設置為0,,或者在 NSExtensionActivationRule 中不添加該類型即可,。
提示:如果你的分享擴展或iOS中的Action擴展需要訪問網頁,那你必須要確保 NSExtensionActivationSupportsWebURLWithMaxCount 關鍵字的值不為0(更多關于在應用擴展中通過JavaScript訪問網頁的內容請參閱Accessing a Webpage),。
你也可以使用 NSExtensionItem 定義的 UTI子 類型以便數(shù)據(jù)檢測器檢測文本信息,,比如電話號碼或通訊地址。
NSExtensionActivationRule字典中的關鍵字足以滿足大多數(shù)應用的過濾需求,。如果你需要做更復雜的過濾,,比如像 public.url 和 public.image 之間的區(qū)別,那么你就得在文本中創(chuàng)建斷言語句,。如果你要創(chuàng)建一個斷言,,那么就將NSExtensionActivationRule關鍵字的值設置為你指定的斷言字符串。(在運行時,,系統(tǒng)會自動將該字符串編譯為 NSPredicate 對象)
比如,,一個應用擴展的附件屬性可以指定為PDF文件,可以這樣寫:
- {extensionItems = ({
- attachments = (
- {
- registeredTypeIdentifiers = (
- "com.adobe.pdf",
- "public.file-url"
- );
- }
- );
- })}
為了指定你的應用擴展可以處理PDF文件,,你可以像這樣創(chuàng)建斷言字符串:
- SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf").@count == 1).@count == 1
開發(fā)過程中,,在你創(chuàng)建斷言語句之前你可以使用TRUEPREDICATE常量(結果為true)測試你的代碼路徑。更多斷言語句的語法知識請參閱Predicate Format String Syntax,。
重要提示:在將你的載體應用上傳App Store之前,,要確保所有的 TRUEPREDICATE 常量已經替換為指定的斷言語句或 NSExtensionActivationRule 關鍵字,不然載體應用會被App Store拒絕,。
配置載體應用以適用于老版本的iOS系統(tǒng)
如果你在載體應用中使用了內嵌框架,,那么它就可以在iOS8.0之后的版本中使用,即便內嵌框架不支持老版本的系統(tǒng)也沒關系,。
使載體應用能做到上述這一點的是 dlopen 命令,,它可以使你使用條件鏈接和加載框架包的機制。你可以使用這個命令來代替編譯時鏈接,,你可以在 Xcode 的 General 選項或 Build Phases 選項中對該命令進行編輯,。其原理就是只有當載體應用在 iOS8.0 或更高的版本中運行時,才會鏈接使用內嵌框架,。
重要提示:如果你的載體應用使用了內嵌框架,,那么就必須要支持arm64架構,,否則會被App Store拒絕。
設置Xcode項目中應用擴展的條件鏈接
1.將每一個應用擴展的運行系統(tǒng)版本設置為iOS8.0或更高,,通常選中Xcode中的target,,在General選項中設置Deployment info。
2.將你載體應用的運行系統(tǒng)版本設置為你想支持的最低iOS版本,。
3.在你的載體應用中,,通過 systemVersion 方法,在運行時檢查判斷iOS的版本,,并判斷是否執(zhí)行dlopen命令,。只有你的載體應用在iOS8.0或更高的版本中運行時才會指定dlopen命令,。
特定的iOS API通過dlopen命令使用內嵌框架,。你必須選擇性的使用這些API,就像使用 dlopen 命令時那樣,。這些API都是 CFBundleRef 的封裝類型:
CFBundleGetFunctionPointerForName
CFBundleGetFunctionPointersforNames
還有來自NSBundle類的方法:
load
loadAndReturnError:
classNamed:
因為你一般會將載體應用的運行系統(tǒng)版本配置為較低的版本,,所以這些API通常都是在運行時檢查,只有確保載體應用在iOS8.0或更高版本中運行時才會使用這些API,。