一、擴展概述html
擴展(Extension)是iOS 8中引入的一個很是重要的新特性。擴展讓app之間的數據交互成爲可能。用戶能夠在app中使用其餘應用提供的功能,而無需離開當前的應用。ios
在iOS 8系統以前,每個app在物理上都是彼此獨立的,app之間不能互訪彼此的私有數據。git
而在引入擴展以後,其餘app能夠與擴展進行數據交換。基於安全和性能的考慮,每個擴展運行在一個單獨的進程中,它擁有本身的bundle, bundle後綴名是.appex。擴展bundle必須包含在一個普通應用的bundle的內部。github
iOS 8系統有6個支持擴展的系統區域,分別是Today、Share、Action、Photo Editing、Storage Provider、Custom keyboard。支持擴展的系統區域也被稱爲擴展點。sql
Today Widget數據庫
對於賽事比分,股票、天氣、快遞這類須要實時獲取的信息,能夠在通知中心的Today視圖中建立一個Today擴展實現。Today擴展又稱爲Widget。數組
Share瀏覽器
在iOS 8以前,用戶只有Facebook,Twitter等有限的幾個分享選項能夠選擇。若是但願將內容分享到Pinterest,開發者則須要一些額外的努力。在iOS 8中,開發者能夠建立自定義的分享選項。安全
Actionsession
action在全部支持的擴展點中擴展性最強的一個。它能夠實現轉換另外一個app上下文中的內容。蘋果在WWDC大會上演示了一個Bing翻譯動做擴展,它能夠將在Safari中選中的文本翻譯成不一樣的語言。
Photo Editing
在iOS 8以前,若是你想爲你的照片添加一個特殊的濾鏡,你須要進入第三方app中,這個過程是至關繁瑣的。在iOS 8中,你能夠直接在Photos中使用第三方app,如Instagram,VSCO cam、Aviary提供的Photo Editing擴展完成對圖片的編輯,而無需離開當前的app。
Storage Provider
Storage Provider讓跨多個文件存儲服務之間的管理變得更簡單。相似Dropbox、Google Drive等存儲提供商經過在iOS 8中提供一個Storage Provider擴展,app直接可使用這些擴展檢索和存儲文件而再也不須要建立沒必要要的拷貝。
Custom Keyboard
蘋果公司在2007年率先推出了觸摸屏鍵盤,但一直沒多大改進。在這一方面,Android則將鍵盤權限開放給了第三方開發者,因此出現了許多像Swype,SwiftKey等優秀的鍵盤輸入法。在iOS 8中,蘋果終於將鍵盤權限開發給了第三方開發者,自定義鍵盤輸入法可讓用戶在整個系統範圍內使用。
2、建立擴展與發佈擴展
在建立擴展以前,你須要建立一個用來包含擴展的常規的app項目。這個包含擴展的app被稱爲containing app。在建立好containg app以後,選擇File->New->Target菜單,從彈出的對話框中選擇一個適當的擴展目標模板。每個擴展目標模板都包含了與擴展點相關的文件和設置。一個containing app能夠包含多個不一樣類型的擴展。
每個擴展目標模板包含一個頭文件和實現文件,一個Info.plist文件,以及一個storyboard文件。Info.plist文件包含了對擴展的配置信息,其中最重要的鍵是NSExtension。下面列出了一個NSExtension可能包含的經常使用鍵值對。
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key> <!--1-->
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>10</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionJavaScriptPreprocessingFile</key> <!--2-->
<string>MyJavaScriptFile</string>
<key>NSExtensionPointVersion</key>
<string>1.0</string>
</dict>
<key>NSExtensionMainStoryboard</key> <!--3-->
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key> <!--4-->
<string>com.apple.ui-services</string>
<key>NSExtensionPrincipalClass</key> <!--5-->
<string>ActionViewController</string>
</dict>
1) NSExtensionActivationRule定義了當前的擴展支持的數據類型及數據項個數,例如當前的設置只支持圖片格式和視頻格式的數據,而且最多不超過10張圖片和1個視頻。
2) NSExtensionJavaScriptPreprocessingFile用於配置與腳本交互的JS腳本文件的名字。
3) NSExtensionMainStoryboard配置擴展的Storyboard文件名。
4) NSExtensionPointIdentifier用於表示擴展點,每個擴展點擁有一個惟一的名字。
5) NSExtensionPrincipalClass配置當擴展啓動時,擴展點首先要實例化的類
爲了將擴展提交蘋果商店,你須要提交你的containg app。而且須要注意,除了擴展必須包含功能之外,同時containg app還須要提供一些功能,而針對OS X平臺的擴展則無此限制。當用戶安裝了你的containg app,containg app中包含的擴展也會一同被安裝。
3、理解擴展如何運做
在安裝擴展以後,擴展並不會自動運行,用戶必須執行特定的操做來啓用擴展。若是是Today擴展,用戶能夠在通知中心的Today視圖中編輯啓用擴展。若是是自定義鍵盤擴展,用戶須要在系統設置的通用選項下的鍵盤選項中啓用自定義鍵盤擴展。而若是是Share擴展,用戶只需點擊系統提供的分享按鈕,便可在分享列表中找到分享擴展。
一個擴展並非一個app,它的生命週期和運行環境不一樣於普通app。在生命週期方面,擴展的生命週期從用戶在另外一個app中選擇了擴展開始,一直到擴展完成了用戶的請求生命週期結束。在運行環境方面,擴展的限制要比普通app更嚴格,擴展的可用內存上限以及可用的API都比普通app要少。嚴格限制擴展的內存是由於在同一時間可能會有多個擴展同時運行,如Widget擴展。若是API聲明包含NS_EXTENSION_UNAVAILABLE宏,則此API在擴展中將不可用,常見的API如:
+ (UIApplication *)sharedApplication NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropriate instead.");
調用擴展的應用稱爲host app,對於Widget擴展,host app就是Today。host app會在擴展的有效生命週期內定義一個擴展上下文。經過擴展上下文,host app能夠和擴展互傳數據。注意,擴展只和host app直接通訊,擴展與containg app以及containing app與host app之間不存在通訊關係,若是擴展須要打開containg app,則經過自定義URL scheme方式實現,而不是直接向containg app發送消息。三者的關係見下圖:
擴展是一個單獨的個體。擴展擁有獨立的target,獨立的bundle文件,獨立的運行進程,獨立的地址空間。這意味着即便你的containing app不在運行,系統也能夠啓動擴展。或者你的containing app處於掛起狀態,一樣不會影響擴展的運行。因此係統能夠單獨對擴展執行優化。擴展與containg app的關係:
4、設計擴展過程當中常見的幾個問題
1. containg app與擴展如何經過擴展上下文互傳數據
在iOS 8中,UIViewController新增了一個擴展上下文屬性extensionContext。來處理containing app與擴展之間的通訊,上下文的類型是NSExtensionContext。假設你如今須要在host app中將一張圖片傳遞給擴展作濾鏡處理,host app中的代碼以下:
UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[[self.imageView image]] applicationActivities:nil];
[self presentViewController:activityViewController animated:YES completion:nil];
當用戶在彈出的Action列表中選擇了擴展,擴展將被啓動,而後在擴展的viewDidLoad方法中,經過extensionContext檢索host app傳回的數據項。擴展中的代碼以下:
- (void)viewDidLoad {
[super viewDidLoad];
NSExtensionItem *imageItem = [self.extensionContext.inputItems firstObject];
if(!imageItem){
return;
}
NSItemProvider *imageItemProvider = [[imageItem attachments] firstObject];
if(!imageItemProvider){
return;
}
// 檢查是否包含文本
if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
[imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *image, NSError *error) {
if(image){
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
}
}];
}
}
上述代碼中,extensionContext表示一個擴展到host app的鏈接。經過extionContent,你能夠訪問一個NSExtensionItem的數組,每個NSExtensionItem項表示從host app傳回的一個邏輯數據單元。你能夠從NSExtensionItem項的attachments屬性中得到附件數據,如音頻,視頻,圖片等。每個附件用NSItemProvider實例表示。上述代碼中NSItemProvider的loadItemForTypeIdentifier實例方法的第一個參數是(NSString *)kUTTypeImage,若是你須要處理的是文本類型,參數則爲(NSString *)kUTTypeText,相應的處理代碼則變成:
if([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeText]){
[imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeText options:nil completionHandler:^(NSAttributedString *string, NSError *error) {
if (string) {
// 在這裏處理文本
}
}];
}
當擴展處理完host app傳回的圖片數據後,它須要將處理好的數據再傳給host app。在擴展中的代碼以下:
-(IBAction)done:(id)sender{
NSExtensionItem* extensionItem = [[NSExtensionItem alloc] init];
[extensionItem setAttachments:@[[[NSItemProvider alloc] initWithItem:[self.imageView image] typeIdentifier:(NSString*)kUTTypeImage]]];
[self.extensionContext completeRequestReturningItems:@[extensionItem] completionHandler:nil];
}
最後一步是host app接收來自擴展傳回的數據,在host app中的代碼以下:
[activityViewController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError * error){
if([returnedItems count] > 0){
NSExtensionItem* extensionItem = [returnedItems firstObject];
NSItemProvider* imageItemProvider = [[extensionItem attachments] firstObject];
if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
[imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *item, NSError *error) {
if(item && !error){
dispatch_async(dispatch_get_main_queue(), ^{
[self.imageView setImage:item];
});
}
}];
}
}
}];
上述代碼主要是經過設置一個completionBlock處理數據回調。
注意,全部的擴展都是一個UIViewController。因此UIViewController的全部生命週期方法,如viewWillAppear:、viewWillDisappear:等在擴展中都是可使用的。
2. 如何在擴展中打開containing app
在通常狀況下,擴展和containing app不存在通訊關係。可是有時候須要在擴展中打開containing app,如iOS 7中預置的日曆Widget。在常規的app中,可使用以下代碼在A app中打開B app:
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:customURL]]) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:customURL]];
}
可是以前有講到,sharedApplication API在擴展中被禁止使用,因此爲了實現一樣的功能,NSExtensionContext定義了一個新的方法用來打開containing app:
- (void)openURL:(NSURL *)URL completionHandler:(void (^)(BOOL success))completionHandler;
在調用此方法以前,須要在containg app中定義一個自定義URL Scheme。定義方法可參見連接,最終的結果以下圖:
在擴展中打開containing app的代碼以下:
- (IBAction)openContainingApp:(id)sender {
NSURL *url = [NSURL URLWithString:@"ExtensionDemo://"];
[self.extensionContext openURL:url completionHandler:^(BOOL success) {
}];
}
3. 如何實現containing app與擴展共享數據
擴展和containing app各自擁有本身的數據容器,雖然擴展內嵌在containing app的內部,可是它們並不能夠互訪彼此的數據。爲了實現containing app與擴展的數據共享,蘋果在iOS 8中引入了一個新的概念——App Group。爲了開啓App Group,找到你的containing app目標,在右側找到Capabilities標籤,定位到App Groups分組,以下圖所示。
而後選擇你須要共享數據的擴展目標,重複執行一次操做,注意兩次的App Group名要相同,不要添加新的條目。當開啓App Group後,你可使用NSUserDefaults方法訪問共享區域,以下述代碼,注意不是[NSUserDefaults standardUserDefaults]:
_sharedUserDefault= [[NSUserDefaults alloc] initWithSuiteName:@"group.com.aegeaon.ExtensionDemo"];
你也可使用NSFileManager的containerURLForSecurityApplicationGroupIdentifier方法訪問共享數據區:
- (BOOL)saveTextByNSFileManager {
NSError *err = nil;
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"];
NSString *value = _textField.text;
BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err];
if (!result) {
NSLog(@"%@",err);
} else {
NSLog(@"save value:%@ success.",value);
}
return result;
}
App Group區域在containing app與擴展之間所處的關係圖:
你可能注意到了,在Xcode 6中iPhone模擬器的位置已經發生了變化。與此同時,在iOS 8 release Note中有提到,app的沙盒結構已經發生了改變,如今它被劃分紅了三個容器,Bundle容器、Data容器、iCloud容器。iOS 8 app沙盒目錄結構以下圖:
爲了具體瞭解沙盒目錄的佈局,使用以下代碼分別在containing app和擴展中打印出App Group目錄,app bundle目錄,以及Document目錄:
- (void)logAppPath
{
//app group路徑
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.aegeaon.ExtensionDemo"];
NSLog(@"app group:\n%@",containerURL.path);
//打印可執行文件路徑
NSLog(@"bundle:\n%@",[[NSBundle mainBundle] bundlePath]);
//打印documents
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];
NSLog(@"documents:\n%@",path);
}
在containing app中執行logAppPath方法的結果以下:
app group:
/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Shared/AppGroup/5B4CFBD8-D95D-4F01-9268-D9F79792147D
bundle:
/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Bundle/Application/EED1F771-A8AD-4A97-97F3-2B0A57936C17/ExtensionDemo.app
documents:
/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Data/Application/95DBF43A-8B4B-426C-9A3A-C1745FCB3FA2/Documents
在擴展中執行logAppPath方法的結果以下:
app group:
/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Shared/AppGroup/5B4CFBD8-D95D-4F01-9268-D9F79792147D
bundle:
/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Bundle/Application/EED1F771-A8AD-4A97-97F3-2B0A57936C17/ExtensionDemo.app/PlugIns/ExpressExt.appex
documents:
~/Documents
其中標註爲紅色的意思是每次運行目錄名都會發生變化。標註爲綠色的表示文件名不會變化的,標準爲橘色也驗證了iOS 8中沙盒目錄被劃分的說法。其中也能夠看出擴展擴展名爲appex,它包含在containing app的PlugIns目錄內。下圖展現了擴展目錄在Finder中的結構:
4. 如何讓擴展訪問到網頁內容
在WWDC上,蘋果演示了在Safari for iOS中使用Bing Action擴展將當前頁面翻譯爲其餘語言。考慮一下,爲了完成這個功能,擴展和瀏覽器之間必定要創建一個鏈接,瀏覽器負責將選中的文本發給擴展,擴展將翻譯的結果發回瀏覽器。爲了實現這個機制,這裏須要藉助一個Javascript腳本,使用JS腳本能夠訪問網頁的DOM。腳本的內容很簡單,只包含兩個方法,腳本文件名爲MyJavaScriptFile.js。代碼以下:
var MyExtensionJavaScriptClass = function() {};
MyExtensionJavaScriptClass.prototype = {
run: function(arguments) {
arguments.completionFunction({"baseURI": document.baseURI});
},
finalize: function(arguments) {
var newContent = arguments["content"];
document.write(newContent);
}
};
var ExtensionPreprocessingJS = new MyExtensionJavaScriptClass;
其中包含一個run()和finalize()方法。當Safari一加載好你的JS文件,就會當即調用run方法,當你在擴展中調用了completeRequestReturningItems:expirationHandler:completion:方法,Safari會調用finalize()方法。在run()方法中,Safari提供了一個arguments參數,使用它能夠利用鍵值對的形式將數據傳給擴展。在上述代碼中,傳給擴展的鍵值對是:@{@"baseURI" : document.baseURI}。在finalize()方法中,當你調用了completeRequestReturningItems:expirationHandler:completion:方法,方法第一個參數的值會傳給finalize()方法的arguments形參。在上述代碼中,finalize()接收到參數後,將內容寫入了當前的文檔。
爲了Safari可以調用正確調用到JS文件,須要在擴展的Info.plist文件中添加以下配置:
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>MyJavaScriptFile</string>
</dict>
在你的擴展中,爲了取得從JS腳本傳回的鍵值對,你須要爲NSItemProvider的方法loadItemForTypeIdentifier:options:completionHandler:指定kUTTypePropertyList數據類型,在取得返回的鍵值字典後,使用NSExtensionJavaScriptPreprocessingResultsKey鍵取值,代碼以下:
NSExtensionContext *context = self.extensionContext;
NSExtensionItem *item = context.inputItems.firstObject;
NSItemProvider *provider = item.attachments.firstObject;
[provider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList
options:nil
completionHandler:^(id<NSSecureCoding> item, NSError *error) {
NSDictionary *results = (NSDictionary *)item;
NSString *baseURI = [[results objectForKey:NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:@"baseURI"];
NSLog(@"%@", baseURI);
}];
爲了在擴展中將處理後的結果傳給腳本,你須要使用NSItemProvider的initWithItem:typeIdentifier:包裝鍵值對。代碼以下:
NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"content":@"Hello World"}} typeIdentifier:(NSString *)kUTTypePropertyList]];
[[self extensionContext] completeRequestReturningItems:@[extensionItem] expirationHandler:nil completion:nil];
5. 如何在containing app與擴展之間共享代碼
iOS 8中,你能夠內嵌一個framework文件在擴展和containing app之間共享代碼。假設你但願在你的containing app與擴展之間共享圖片處理的代碼,此時你能夠將代碼打包成framework文件,內嵌到兩個目標中。對於內嵌框架中的代碼,確保不包含擴展不容許使用的API。
如何將代碼打包成framework文件這裏就不敖述了,感興趣的同窗能夠參見:http://blog.sina.com.cn/s/blog_407fb5bc01013v6s.html。當你建立好.framework文件後,你能夠直接將.framework文件同時拖入containing app和擴展中,以下圖所示:
這裏使用公司ILSLib目錄下的的MagicalRecord21.framework文件做爲素材,講解如何在containing app和自定義鍵盤擴展之間實現共享Core Data數據庫。在你的擴展和containing app中中配置好引用頭文件。分別在containing app的AppDelegate文件的application: didFinishLaunchingWithOptions: launchOptions與自定義鍵盤擴展的UIInputViewController子類文件中viewDidLoad方法中添加以下代碼:
[MagicalRecord setupCoreDataStackWithStoreNamed:@"demo.sqlite"];
上述代碼分別對containing app和擴展執行Core Data棧初始化,其中包括數據模型、sqlite存儲文件等配置。運行containing app,此時AppDelegate中的數據庫配置代碼會被執行,接着打開系統設置中的通用選項下的鍵盤選項,在這裏啓用自定義鍵盤。而後回到containing app,切換到自定義鍵盤擴展,此時自定義鍵盤擴展中viewDidLoad方法中的數據庫配置代碼執行,可是控制檯出現錯誤提示:
CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:~/Library/Application%20Support/CustomKeyboardExt/demo.sqlite -- file:/// options:(null) ... returned error Error Domain=NSCocoaErrorDomain Code=512 "The operation couldn’t be completed. (Cocoa error 512.)" UserInfo=0x7b48a720 {reason=Failed to create file; code = 2} with userInfo dictionary {
reason = "Failed to create file; code = 2";
}
上述錯誤表示在擴展的~/Library/Application%20Support/CustomKeyboardExt/demo.sqlite目錄建立.sqlite文件失敗。翻閱MagicalRecord源代碼(須要從github從新下載源代碼,.framework看不到源代碼),其中在建立.sqlite存儲文件路徑的代碼中會發現:
+ (NSURL *) MR_urlForStoreName:(NSString *)storeFileName {
NSArray *paths = [NSArray arrayWithObjects:[self MR_applicationDocumentsDirectory], [self MR_applicationStorageDirectory], nil];
NSFileManager *fm = [[NSFileManager alloc] init];
for (NSString *path in paths) {
NSString *filepath = [path stringByAppendingPathComponent:storeFileName];
if ([fm fileExistsAtPath:filepath]) {
return [NSURL fileURLWithPath:filepath];
}
}
//set default url
return [NSURL fileURLWithPath:[[self MR_applicationStorageDirectory] stringByAppendingPathComponent:storeFileName]];
}
其中MR_applicationStorageDirectory方法返回的是Application Support目錄,而這個目錄是處在Library目錄內的。上文中已經講過,擴展沒有Documents目錄,一樣也是沒有Library目錄。因此文件建立會發生失敗。爲了實現擴展與containing app之間共享.sqlite文件,這裏須要將.sqlite文件建立在App Group區域。問題是MagicalRecord21.framework文件只暴露了頭文件,沒法對其源文件中的MR_urlForStoreName:方法作修改。這裏使用Objective-C的動態運行時技術——Method Swizzling,在運行時將MR_urlForStoreName:方法的實現使用新的實現進行替換。 (注:這裏能夠直接給setupCoreDataStackWithStoreNamed方法傳遞一個包含文件路徑的URL類型參數實現修改.sqlite文件的存放位置,methodSwizzling只是另外一種通用處理方法)
首先須要爲自定義鍵盤擴展建立一個Category文件NSPersistentStore+Tracking.h/m,.m文件中的完整的代碼以下:
#import "NSPersistentStore+Tracking.h"
#import <objc/runtime.h>
#import <MagicalRecord21/CoreData+MagicalRecord.h>
static NSString * const kGroupName = @"group.com.aegeaon.ExtensionDemo";
static NSString * const kContainingDirectory = @"CoreDataStore/";
@implementation NSPersistentStore (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(MR_urlForStoreName:);
SEL swizzledSelector = @selector(ILS_urlForStoreName:);
SwizzleClassMethod(class, originalSelector, swizzledSelector);
});
}
void SwizzleClassMethod(Class c, SEL orig, SEL new) {
Method origMethod = class_getClassMethod(c, orig);
Method newMethod = class_getClassMethod(c, new);
c = object_getClass((id)c);
if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
else
method_exchangeImplementations(origMethod, newMethod);
}
#pragma mark - Method Swizzling
+ (NSURL *) ILS_urlForStoreName:(NSString *)storeFileName {
NSURL *storeURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:kGroupName];
storeURL = [storeURL URLByAppendingPathComponent:[kContainingDirectory stringByAppendingString:storeFileName]];
return storeURL;
}
@end
在當前的代碼一載入內存,load方法將被執行,它比AppDelegate的application: didFinishLaunchingWithOptions: launchOptions方法要先被執行,上述代碼會將MR_urlForStoreName:的實現替換成ILS_urlForStoreName:,在ILS_urlForStoreName:方法中,使用NSFileManager的containerURLForSecurityApplicationGroupIdentifier方法設定App Group,最終的.sqlite文件將保存在App Group目錄內的CoreDataStore目錄下。一樣須要爲containing app中使用此方法,能夠直接將NSPersistentStore+Tracking.h/m拖入containing app目標內。再次運行自定義鍵盤擴展,數據庫文件已成功保存到App Group中。以下圖:
同時被共享的代碼框架MagicalRecord21.framework被containg app和擴展共享,雙方共用一個框架文件,以下圖:
6. 如何在擴展中處理長時間任務
用戶但願在擴展完成他們的任務以後可以當即返回到host app中。可是若是擴展執行的任務是一個長時間任務,好比下載。在這種狀況下,須要使用NSURLSession來建立一個下載session,並初始化一個後臺下載任務。當擴展初始化了上傳下載任務後,就算是完成了host app的請求,擴展就能夠被終止。這不會影響到任務的結果。若是當後臺任務完成後,你的擴展不在運行,系統將在後臺啓動你的contaiing app並調用appdelegate的aplication:handleEventsForBackgroundURLSession:completionHandler:方法。爲了在擴展中初始化一個後臺的NSURLSession任務,你必須設置一個containing app和擴展均可以訪問的共享容器。
相關代碼以下:
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」];
config.sharedContainerIdentifier = @"com.mycompany.myappgroupidentifier";
mySession = [NSURLSession sessionWithConfiguration:config delegate:selfdelegateQueue:nil];
}
return mySession;
}