[譯]AppExtension程式設計指南:擴充套件基礎4

taoZen發表於2018-11-20

App Extension Programming Guide-App Extension Essentials AppExtension程式設計指南:擴充套件基礎4

Handling Common Scenarios 常見問題的處理方案

iOS8/OS X v10.10

常見問題的處理方案

當編寫自定義程式碼以執行app擴充套件任務時,你可能需要處理一些其他多種型別擴充套件也會出現的情況。在這一章節中,我們將幫助你如何應對和處理這些常見的問題。

使用內嵌框架共享程式碼

你可以建立一個內嵌框架,用於在應用擴充套件和它的主應用程式(containing app)之間共享程式碼。比如,你在照片編輯擴充套件中開發了圖片濾鏡功能,那麼同時該擴充套件的主應用程式containing app也有這個功能,那麼你可以將實現該功能的程式碼封裝成一個框架,並在擴充套件target和主應用程式target中嵌入這個框架。

你要確保你建立的內嵌框架不包含應用擴充套件不能使用的API。這類API一般使用unavailability巨集來標記,比如像 NS_EXTENSION_UNAVAILABLE

如果你建立的內嵌框架中包含應用擴充套件不能使用的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時會被拒絕。(如“建立應用擴充套件”章節中介紹的,所有應用擴充套件都要支援arm64架構。)

在配置配置Xcode專案時,必須在Copy Files編譯階段選擇“Frameworks”作為內嵌框架的目標。

重要提示:我們通常要選擇 Frameworks 作為 Copy Files 編譯階段目標。如果你將其設定為 SharedFramework,那麼上傳App Store時會被拒絕的。

你可以讓containing app支援iOS7或更早的版本,但當在iOS8或更新的版本中執行時,要特別注意內嵌框架的安全性。詳細內容可以參閱 Deploying a Containing App to Older Versions of iOS。

有關建立和使用內嵌框架的更多內容,請觀看WWDC 2014的視訊“Building Modern Frameworks”

與Containing App共享資料

應用擴充套件和它的containing app的安全域是有區別的。即便擴充套件包是巢狀在containing app包中的。預設情況下,應用擴充套件和containing app是不能直接訪問對方的容器的。

BACKGROUND 要了解容器,閱讀 About the iOS File System 中的 File System Programming Guid.

不過你可以通過資料共享來實現這個願望。比如,你希望應用擴充套件和它的containing app共享一個單一的大資料集。比如prerendered assets。

要實現資料共享,我們要使用Xcode或者開發者入口網站允許應用擴充套件和它的containing app成為一個應用組,然後在開發者入口網站中註冊應用組,並指明在containing app中使用該應用組。關於應用組的知識請查閱 Entitlement Key Reference 文件的 Adding an App to an App Group 章節。

當你設定好應用組後,應用擴充套件和它的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是如何通過共享容器實現資料共享的.

Figure 4-1應用擴充套件的容器與其containing app的容器是不同的。

image

重要提示:如果你的應用擴充套件使用NSURLSession類執行後臺的上傳下載任務時,你必須要設定一個共享容器,這樣擴充套件和containing app就可以訪問到轉換傳輸的資料。後臺上傳下載的更多知識請參閱 Performing Uploads and Downloads。

如果你設定了共享容器,那麼containing app和它包含的允許參與資料分享的擴充套件就可以對共享容器裡的內容進行讀寫操作了。同時你還必須要對資料的操作進行同步,以避免資料損壞或出錯。使用UIDocument類、Core Data或者SQLite可以幫你可以讓使用者通過要求Safari執行JS檔案來訪問網路內容,並將結果返回到擴充套件。

版本說明 在iOS 8.2及更高版本中,您也可以使用UIDocument該類來協調共享資料訪問。 在iOS 9及更高版本中,您可以NSFileCoordinator直接使用該類進行共享資料訪問,但是如果您這樣做,則必須NSFilePresenter在應用擴充套件轉換為後臺時刪除物件。

訪問網頁

在分享擴充套件(iOS與OS X平臺)和Action擴充套件(iOS平臺)中,一般都允許使用者使用Safari瀏覽器訪問網頁並通過執行JavaScript指令碼,並將結果返回到擴充套件中。你也可以在你的擴充套件執行之前(適用於兩個平臺)或執行完任務之後(僅適用於iOS平臺)通過JavaScript檔案修改網頁內容。比如分享擴充套件,它可以幫助使用者分享網頁上的內容,或者iOS上的Action擴充套件可能會顯示當前網頁的指定翻譯內容。

如果想新增網頁訪問和操作應用擴充套件,那麼需要遵循下面幾個步驟: 1.建立一個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系統的應用擴充套件中,如果你希望Safari在擴充套件執行完任務後更新網頁,那麼你要向JavaScript檔案中傳入值。(在這一步中也使用NSItemProvider類。)

為了告知Safari你的應用擴充套件中包含一個JavaScript檔案,你需要在應用擴充套件的Info.plist檔案中,向NSExtensionAttributes字典新增NSExtensionJavaScriptPreprocessingFile關鍵字來指明你的JavaScript檔案。這個鍵的值就是你希望當你的應用擴充套件執行前,Safari要載入的JavaScript檔案的名稱。比如:

<key>NSExtensionAttributes</key>
        <dict>
            <key>NSExtensionJavaScriptPreprocessingFile</key>
            <string>MyJavaScriptFile</string> <!-- Do not include the ".js" filename extension -->
        </dict>

複製程式碼

在iOS和OS X平臺中,在你自定義的JavaScript類中可以定義一個run()函式,該函式就是Safari載入JavaScript檔案的入口。在run()函式中,Safari提供了一個名為completionFunction的引數,你可以使用鍵值物件的形式將結果傳給應用擴充套件。

在iOS平臺中,你還可以定義一個finalize()函式,當應用擴充套件在任務結束階段呼叫completeRequestReturningItems:expirationHandler:completion:方法時Safari會呼叫finalize()函式。在該函式中,可以通過向completeRequestReturningItems:expirationHandler:completion:方法傳值,來改變網頁內容。

比如,你的iOS應用擴充套件需要基於一個網頁URI啟動,並且當它結束執行時改變網頁的背景色,那麼你需要這樣寫JavaScript程式碼:

清單4-1示例run()和finalize()函式

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: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平臺中,你需要編寫程式碼來處理run()函式返回的值,為獲取到字典中的值,我們需要指定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() 函式是在當應用擴充套件執行完任務後傳參並呼叫的,建立一個含有我們需要處理的值的字典,然後用NSItemProviderinitWithItem:typeIdentifier:方法來封裝該字典。比如當擴充套件執行完任務後我們想讓網頁變為紅色,我們可以這樣寫:

NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
    extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"bgColor":@"red"}} typeIdentifier:(NSString *)kUTTypePropertyList]];
    [[self extensionContext] completeRequestReturningItems:@[extensionItem] completion:nil];
 
複製程式碼

執行上傳下載任務

使用者一般的操作習慣都傾向於當使用你的應用擴充套件完成某個任務後,可以將結果立即反饋在使用擴充套件的應用中。如果一個擴充套件要處理的任務包含較長時間的上傳下載操作時,你要確保當你的應用擴充套件關閉後能繼續完成該任務。為實現這個功能,我們需要使用NSURLSession類建立一個URL會話並建立後臺的上傳下載任務。

提示:你可以回想一下其他型別的後臺任務,比如後臺支援VoIP、後臺播放音樂,這些是不能用應用擴充套件去實現的。更多資訊請參閱Respond to the Host App’s Request。

當你的應用擴充套件準備好上傳下載任務後,擴充套件會完成呼叫它的應用發出的請求,並在不影響上傳下載任務的前提下終止擴充套件。更多關於擴充套件處理載體應用請求的知識請參閱Respond to the Host App’s Request。在iOS系統中,如果你的應用擴充套件在執行完後臺任務時並沒有在執行,那麼系統會自動在後臺執行擴充套件的載體應用,並呼叫application:handleEventsForBackgroundURLSession:completionHandler: 代理方法。

重要提示:如果你的應用擴充套件在後臺建立了 NSURLSession 任務,那麼你必須要設定一個共享容器,以確保擴充套件和載體應用實現資料共享。我們可以在 NSURLSessionConfiguration 類中使用sharedContainerIdentifier屬性來指定一個共享容器的標示符,然後我們就可以通過該標示符獲取到共享容器。請參閱 Sharing Data with Your Containing App 文件來設定共享容器。

下面的例子展示瞭如何配置一個URL會話,並建立一個下載任務:

NSURLSession *mySession = [self configureMySession];
NSURL *url = [NSURL URLWithString:@"http://www.example.com/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;
}

複製程式碼

因為在單位時間內只能由一個程式使用後臺會話,所以你需要為載體應用中的所有擴充套件建立不同的後臺會話(每個後臺會話都要有一個唯一的標示符)。在這裡我們建議當載體應用在後臺處理擴充套件的任務時,只使用一個該擴充套件建立的後臺會話。如果你要執行其他的網路相關的任務,那麼就要建立相應的URL會話。

如果你需要在後臺建立URL會話之前完成載體應用的請求,那麼要確保建立和使用會話的程式碼是有效可執行的。當你的擴充套件呼叫 completeRequestReturningItems:completionHandler: 方法告知主叫應用已經完成相關請求後,系統就可以隨時終止你的應用擴充套件。

為分享和Action擴充套件申明支援的資料型別

在你的分享或Action擴充套件中,在它們的工作中可能會使用到一些資料,並且這些資料的型別各不相同。為了確保只有當使用者在載體應用中選擇了你的擴充套件支援的資料型別時,才會展示你的擴充套件功能。你需要在擴充套件的Info.plist屬性列表檔案中新增 NSExtensionActivationRule 關鍵字。你也可以使用該關鍵字指定擴充套件處理每種型別的最大數目。當你的應用擴充套件執行時,系統會將NSExtensionActivationRule鍵的值與擴充套件項的attachments屬性中的資訊進行比較。關於 NSExtensionActivationRule 關鍵字的詳細資訊可以參閱 Action Extension Keys文件中的 Information Property List Key Reference 章節。

比如,你可以申明你的分享擴充套件支援最多處理10張圖片,一部影片和一個網站URL。您可以使用以下字典作為該NSExtensionAttributes鍵的值:

<key>NSExtensionAttributes</key>
    <dict>
        <key>NSExtensionActivationRule</key>
        <dict>
            <key>NSExtensionActivationSupportsImageWithMaxCount</key>
            <integer>10</integer>
            <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
            <integer>1</integer>
            <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
            <integer>1</integer>
        </dict>
    </dict>              
複製程式碼

如果你想指定不支援的資料型別,那麼你可以將該型別的值設定為0,或者在 NSExtensionActivationRule 中不新增該型別即可。

注意:如果你的分享擴充套件或iOS中的Action擴充套件需要訪問網頁,那你必須要確保 NSExtensionActivationSupportsWebURLWithMaxCount 關鍵字的值不為0(更多關於在應用擴充套件中通過JavaScript訪問網頁的內容請參閱Accessing a Webpage

你也可以使用 NSExtensionItem 定義的 UTI子 型別以便資料檢測器檢測文字資訊,比如電話號碼或通訊地址。

NSExtensionActivationRule字典中的鍵足以滿足大多數應用的過濾需求。如果你需要做更復雜的過濾,比如像 public.urlpublic.image 之間的區別,那麼你就得在文字中建立斷言語句。如果你要建立一個斷言,那麼就將NSExtensionActivationRule關鍵字的值設定為你指定的斷言字串。(在執行時,系統會自動將該字串編譯為 NSPredicate 物件

比如,一個應用擴充套件的附件屬性可以指定為PDF檔案,可以這樣寫:

{extensionItems = ({
    attachments = ({
        registeredTypeIdentifiers = (
            "com.adobe.pdf",
            "public.file-url"
        );
    });
})}
複製程式碼

為了指定你的應用擴充套件可以處理PDF檔案,你可以像這樣建立斷言字串:

SUBQUERY (
    extensionItems,
    $extensionItem,
    SUBQUERY (
        $extensionItem.attachments,
        $attachment,
        ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf"
    ).@count == $extensionItem.attachments.@count
).@count == 1
複製程式碼

以下是更復雜的斷言語句的示例:

SUBQUERY (
    extensionItems,
    $extensionItem,
    SUBQUERY (
        $extensionItem.attachments,
        $attachment,
        ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.action-one" ||
        ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.action-two"
    ).@count == $extensionItem.attachments.@count
).@count == 1

複製程式碼

此語句遍歷一個NSExtensionItem物件陣列,其次是遍歷attachments每個擴充套件項中的陣列。對於每個附件,謂詞評估附件中每個表示的統一型別識別符號(UTI)。當附件表示UTI符合兩個不同的指定UTI中的任何一個(您在每個UTI-CONFORMS-TO操作員的右側看到)時,收集該UTI以進行最終比較測試。TRUE如果應用程式副檔名僅提供了一個支援UTI的擴充套件項附件,則返回最後一行。

開發過程中,在你建立斷言語句之前你可以使用TRUEPREDICATE常量(結果為true)測試你的程式碼路徑。更多斷言語句的語法知識請參閱Predicate Format String Syntax。

重要提示:在將你的載體應用上傳App Store之前,要確保所有的 TRUEPREDICATE 常量已經替換為指定的斷言語句或 NSExtensionActivationRule 關鍵字,不然載體應用會被App Store拒絕。

配置載體應用以適用於老版本的iOS系統

如果你在主體應用中使用了內嵌框架,那麼它就可以在iOS8.0之後的版本中使用,即便內嵌框架不支援老版本的系統也沒關係。

使主體應用能做到上述這一點的是 dlopen 命令,它可以使你使用條件連結和載入框架包的機制。你可以使用這個命令來代替編譯時連結,你可以在 Xcode 的 General 選項或 Build Phases 選項中對該命令進行編輯。其原理就是隻有當主體應用在 iOS8.0 或更高的版本中執行時,才會連結使用內嵌框架。

您必須在有條件地 framework bundle的程式碼語句中使用Objective-C而不是Swift。您的應用程式的其餘部分可以用任何一種語言編寫,內嵌框架本身也可以用任何一種語言編寫。

呼叫之後dlopen,使用以下型別的語句訪問內嵌框架類:

MyLoadedClass *loadedClass = [[NSClassFromString (@"MyClass") alloc] init]; 
複製程式碼

重要提示:如果你的主體應用使用了內嵌框架,那麼就必須要支援arm64架構,否則會被App Store拒絕。

設定Xcode專案中應用擴充套件的條件連結

1.將每一個應用擴充套件的執行系統版本設定為iOS8.0或更高,通常選中Xcode中的target,在General選項中設定Deployment info。 2.將你主體應用的執行系統版本設定為你想支援的最低iOS版本。 3.在你的主體應用中,通過 systemVersion 方法,在執行時檢查判斷iOS的版本,並判斷是否執行dlopen命令。只有你的載體應用在iOS8.0或更高的版本中執行時才會指定dlopen命令。進行此呼叫時,請務必使用Objective-C,而不是Swift。

特定的iOS API通過dlopen命令使用內嵌框架。你必須選擇性的使用這些API,就像使用 dlopen 命令時那樣。這些API都是 CFBundleRef 的封裝型別:

CFBundleGetFunctionPointerForName

CFBundleGetFunctionPointersforNames

還有來自NSBundle類的方法:

loadloadAndReturnError: classNamed:

因為你一般會將載體應用的執行系統版本配置為較低的版本,所以這些API通常都是在執行時檢查,只有確保載體應用在iOS8.0或更高版本中執行時才會使用這些API。

相關文章