IOS8系列之應用擴展

一、擴展概述
擴展(Extension)是iOS 8中引入的一個非常重要的新特性。擴展讓app之間的數據交互成爲可能。用戶可以在app中使用其他應用提供的功能,而無需離開當前的應用。
 
在iOS 8系統之前,每一個app在物理上都是彼此獨立的,app之間不能互訪彼此的私有數據

而在引入擴展之後,其他app可以與擴展進行數據交換。基於安全和性能的考慮,每一個擴展運行在一個單獨的進程中,它擁有自己的bundle, bundle後綴名是.appex。擴展bundle必須包含在一個普通應用的bundle的內部。

iOS 8系統有6個支持擴展的系統區域,分別是Today、Share、Action、Photo Editing、Storage Provider、Custom keyboard。支持擴展的系統區域也被稱爲擴展點。
 
Today Widget
對於賽事比分,股票、天氣、快遞這類需要實時獲取的信息,可以在通知中心的Today視圖中創建一個Today擴展實現。Today擴展又稱爲Widget。

Share
在iOS 8之前,用戶只有Facebook,Twitter等有限的幾個分享選項可以選擇。如果希望將內容分享到Pinterest,開發者則需要一些額外的努力。在iOS 8中,開發者可以創建自定義的分享選項。

Action
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中,蘋果終於將鍵盤權限開發給了第三方開發者,自定義鍵盤輸入法可以讓用戶在整個系統範圍內使用。

二、創建擴展與發佈擴展
在創建擴展之前,你需要創建一個用來包含擴展的常規的app項目。這個包含擴展的app被稱爲containing app。在創建好containg app之後,選擇File->New->Target菜單,從彈出的對話框中選擇一個適當的擴展目標模板。每一個擴展目標模板都包含了與擴展點相關的文件和設置。一個containing app可以包含多個不同類型的擴展。
 
每一個擴展目標模板包含一個頭文件和實現文件,一個Info.plist文件,以及一個storyboard文件。Info.plist文件包含了對擴展的配置信息,其中最重要的鍵是NSExtension。下面列出了一個NSExtension可能包含的常用鍵值對。
  1. <key>NSExtension</key> 
  2.  
  3.  <dict> 
  4.  
  5.     <key>NSExtensionAttributes</key> 
  6.  
  7.     <dict> 
  8.  
  9.             <key>NSExtensionActivationRule</key> <!--1--> 
  10.  
  11.             <dict> 
  12.  
  13.                 <key>NSExtensionActivationSupportsImageWithMaxCount</key> 
  14.  
  15.                 <integer>10</integer> 
  16.  
  17.                 <key>NSExtensionActivationSupportsMovieWithMaxCount</key> 
  18.  
  19.                 <integer>1</integer> 
  20.  
  21.                  </dict> 
  22.  
  23.             <key>NSExtensionJavaScriptPreprocessingFile</key> <!--2--> 
  24.  
  25.             <string>MyJavaScriptFile</string> 
  26.  
  27.            <key>NSExtensionPointVersion</key> 
  28.  
  29.            <string>1.0</string> 
  30.  
  31.        </dict> 
  32.  
  33.      <key>NSExtensionMainStoryboard</key>  <!--3--> 
  34.  
  35.      <string>MainInterface</string> 
  36.  
  37.        <key>NSExtensionPointIdentifier</key>  <!--4--> 
  38.  
  39.        <string>com.apple.ui-services</string> 
  40.  
  41.      <key>NSExtensionPrincipalClass</key>  <!--5--> 
  42.  
  43.        <string>ActionViewController</string> 
  44.  
  45. </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中包含的擴展也會一同被安裝。
 
三、理解擴展如何運作
在安裝擴展之後,擴展並不會自動運行,用戶必須執行特定的操作來啓用擴展。如果是Today擴展,用戶可以在通知中心的Today視圖中編輯啓用擴展。如果是自定義鍵盤擴展,用戶需要在系統設置的通用選項下的鍵盤選項中啓用自定義鍵盤擴展。而如果是Share擴展,用戶只需點擊系統提供的分享按鈕,即可在分享列表中找到分享擴展。
 
一個擴展並不是一個app,它的生命週期和運行環境不同於普通app。在生命週期方面,擴展的生命週期從用戶在另一個app中選擇了擴展開始,一直到擴展完成了用戶的請求生命週期結束。在運行環境方面,擴展的限制要比普通app更嚴格,擴展的可用內存上限以及可用的API都比普通app要少。嚴格限制擴展的內存是因爲在同一時間可能會有多個擴展同時運行,如Widget擴展。如果API聲明包含NS_EXTENSION_UNAVAILABLE宏,則此API在擴展中將不可用,常見的API如:
  1. + (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的關係:

四、設計擴展過程中常見的幾個問題
1. containg app與擴展如何通過擴展上下文互傳數據
 
在iOS 8中,UIViewController新增了一個擴展上下文屬性extensionContext。來處理containing app與擴展之間的通信,上下文的類型是NSExtensionContext。假設你現在需要在host app中將一張圖片傳遞給擴展做濾鏡處理,host app中的代碼如下:
  1. UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[[self.imageView image]] applicationActivities:nil]; 
  2.  
  3. [self presentViewController:activityViewController animated:YES completion:nil]; 
當用戶在彈出的Action列表中選擇了擴展,擴展將被啓動,然後在擴展的viewDidLoad方法中,通過extensionContext檢索host app傳回的數據項。擴展中的代碼如下:
  1. - (void)viewDidLoad { 
  2.  
  3.     [super viewDidLoad]; 
  4.  
  5.     NSExtensionItem *imageItem = [self.extensionContext.inputItems firstObject]; 
  6.  
  7.     if(!imageItem){ 
  8.  
  9.         return
  10.  
  11.     } 
  12.  
  13.     NSItemProvider *imageItemProvider = [[imageItem attachments] firstObject]; 
  14.  
  15.     if(!imageItemProvider){ 
  16.  
  17.         return
  18.  
  19.     } 
  20.  
  21.    // 檢查是否包含文本 
  22.  
  23.     if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){ 
  24.  
  25.         [imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *image, NSError *error) { 
  26.  
  27.             if(image){ 
  28.  
  29.                 dispatch_async(dispatch_get_main_queue(), ^{ 
  30.  
  31.                     self.imageView.image = image; 
  32.  
  33.                 }); 
  34.  
  35.             } 
  36.  
  37.         }]; 
  38.  
  39.         
  40.  
  41.     } 
  42.  
上述代碼中,extensionContext表示一個擴展到host app的連接。通過extionContent,你可以訪問一個NSExtensionItem的數組,每一個NSExtensionItem項表示從host app傳回的一個邏輯數據單元。你可以從NSExtensionItem項的attachments屬性中獲得附件數據,如音頻,視頻,圖片等。每一個附件用NSItemProvider實例表示。上述代碼中NSItemProvider的loadItemForTypeIdentifier實例方法的第一個參數是(NSString *)kUTTypeImage,如果你需要處理的是文本類型,參數則爲(NSString *)kUTTypeText,相應的處理代碼則變成:
  1. if([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeText]){ 
  2.  
  3.     [imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeText options:nil completionHandler:^(NSAttributedString *string, NSError *error) { 
  4.  
  5.         if (string) { 
  6.  
  7.             // 在這裏處理文本 
  8.  
  9.         } 
  10.  
  11.     }]; 
  12.  
當擴展處理完host app傳回的圖片數據後,它需要將處理好的數據再傳給host app。在擴展中的代碼如下:
  1. -(IBAction)done:(id)sender{ 
  2.  
  3.     NSExtensionItem* extensionItem = [[NSExtensionItem alloc] init]; 
  4.  
  5.     [extensionItem setAttachments:@[[[NSItemProvider alloc] initWithItem:[self.imageView image] typeIdentifier:(NSString*)kUTTypeImage]]]; 
  6.  
  7.     
  8.  
  9.     [self.extensionContext completeRequestReturningItems:@[extensionItem] completionHandler:nil]; 
  10.  
最後一步是host app接收來自擴展傳回的數據,在host app中的代碼如下:
  1. [activityViewController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError * error){ 
  2.  
  3.         if([returnedItems count] > 0){ 
  4.  
  5.             NSExtensionItem* extensionItem = [returnedItems firstObject]; 
  6.  
  7.             NSItemProvider* imageItemProvider = [[extensionItem attachments] firstObject]; 
  8.  
  9.             if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){ 
  10.  
  11.                 [imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *item, NSError *error) { 
  12.  
  13.                     if(item && !error){ 
  14.  
  15.                         dispatch_async(dispatch_get_main_queue(), ^{ 
  16.  
  17.                             [self.imageView setImage:item]; 
  18.  
  19.                         }); 
  20.  
  21.                     } 
  22.  
  23.                 }]; 
  24.  
  25.                 
  26.  
  27.             } 
  28.  
  29.         } 
  30.  
  31.     }]; 
上述代碼主要是通過設置一個completionBlock處理數據回調。
 
注意,所有的擴展都是一個UIViewController。所以UIViewController的所有生命週期方法,如viewWillAppear:、viewWillDisappear:等在擴展中都是可以使用的。
 
2. 如何在擴展中打開containing app
 
在一般情況下,擴展和containing app不存在通信關係。但是有時候需要在擴展中打開containing app,如iOS 7中預置的日曆Widget。在常規的app中,可以使用如下代碼在A app中打開B app:
  1. if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:customURL]]) { 
  2.  
  3.        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:customURL]]; 
  4.  
  5.    } 
但是之前有講到,sharedApplication API在擴展中被禁止使用,所以爲了實現同樣的功能,NSExtensionContext定義了一個新的方法用來打開containing app:
  1. - (void)openURL:(NSURL *)URL completionHandler:(void (^)(BOOL success))completionHandler; 
在調用此方法之前,需要在containg app中定義一個自定義URL Scheme。定義方法可參見鏈接,最終的結果如下圖:

在擴展中打開containing app的代碼如下:
  1. - (IBAction)openContainingApp:(id)sender { 
  2.  
  3.     NSURL *url = [NSURL URLWithString:@"ExtensionDemo://"]; 
  4.  
  5.     [self.extensionContext openURL:url completionHandler:^(BOOL success) { 
  6.  
  7.     }]; 
  8.  
 
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]:
  1. _sharedUserDefault= [[NSUserDefaults alloc] initWithSuiteName:@"group.com.aegeaon.ExtensionDemo"]; 
你也可以使用NSFileManager的containerURLForSecurityApplicationGroupIdentifier方法訪問共享數據區:
  1. - (BOOL)saveTextByNSFileManager { 
  2.  
  3.     NSError *err = nil; 
  4.  
  5.     NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"]; 
  6.  
  7.     containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"]; 
  8.  
  9.   
  10.  
  11.     NSString *value = _textField.text; 
  12.  
  13.     BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err]; 
  14.  
  15.     if (!result) { 
  16.  
  17.         NSLog(@"%@",err); 
  18.  
  19.     } else { 
  20.  
  21.         NSLog(@"save value:%@ success.",value); 
  22.  
  23.     } 
  24.  
  25.   
  26.  
  27.     return result; 
  28.  
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目錄:
  1. - (void)logAppPath 
  2.  
  3.  
  4.     //app group路徑 
  5.  
  6.     NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.aegeaon.ExtensionDemo"]; 
  7.  
  8.     NSLog(@"app group:\n%@",containerURL.path); 
  9.  
  10.     
  11.  
  12.     //打印可執行文件路徑 
  13.  
  14.     NSLog(@"bundle:\n%@",[[NSBundle mainBundle] bundlePath]); 
  15.  
  16.     
  17.  
  18.     //打印documents 
  19.  
  20.     NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 
  21.  
  22.     NSString *path = [paths objectAtIndex:0]; 
  23.  
  24.     NSLog(@"documents:\n%@",path); 
  25.  
在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。代碼如下:
  1. var MyExtensionJavaScriptClass = function() {}; 
  2.  
  3. MyExtensionJavaScriptClass.prototype = { 
  4.  
  5.     run: function(arguments) { 
  6.  
  7.         arguments.completionFunction({"baseURI": document.baseURI}); 
  8.  
  9.     }, 
  10.  
  11.     finalize: function(arguments) { 
  12.  
  13.         var newContent = arguments["content"]; 
  14.  
  15.         document.write(newContent); 
  16.  
  17.     } 
  18.  
  19. }; 
  20.  
  21. 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文件中添加如下配置:
  1. <key>NSExtensionAttributes</key> 
  2.  
  3.  <dict> 
  4.  
  5.       <key>NSExtensionJavaScriptPreprocessingFile</key> 
  6.  
  7.       <string>MyJavaScriptFile</string> 
  8.  
  9.   </dict> 
在你的擴展中,爲了取得從JS腳本傳回的鍵值對,你需要爲NSItemProvider的方法loadItemForTypeIdentifier:options:completionHandler:指定kUTTypePropertyList數據類型,在取得返回的鍵值字典後,使用NSExtensionJavaScriptPreprocessingResultsKey鍵取值,代碼如下:
  1. NSExtensionContext *context = self.extensionContext; 
  2.  
  3.     NSExtensionItem *item = context.inputItems.firstObject; 
  4.  
  5.     NSItemProvider *provider = item.attachments.firstObject; 
  6.  
  7.     
  8.  
  9.     [provider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList 
  10.  
  11.                                 options:nil 
  12.  
  13.                       completionHandler:^(id<NSSecureCoding> item, NSError *error) { 
  14.  
  15.                           NSDictionary *results = (NSDictionary *)item; 
  16.  
  17.                           NSString *baseURI = [[results objectForKey:NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:@"baseURI"]; 
  18.  
  19.                           NSLog(@"%@", baseURI);                          
  20.  
  21.                       }]; 
爲了在擴展中將處理後的結果傳給腳本,你需要使用NSItemProvider的initWithItem:typeIdentifier:包裝鍵值對。代碼如下:
  1. NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init]; 
  2.  
  3. extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"content":@"Hello World"}} typeIdentifier:(NSString *)kUTTypePropertyList]]; 
  4.  
  5.  [[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方法中添加如下代碼:
  1. [MagicalRecord setupCoreDataStackWithStoreNamed:@"demo.sqlite"]; 
上述代碼分別對containing app和擴展執行Core Data棧初始化,其中包括數據模型、sqlite存儲文件等配置。運行containing app,此時AppDelegate中的數據庫配置代碼會被執行,接着打開系統設置中的通用選項下的鍵盤選項,在這裏啓用自定義鍵盤。然後回到containing app,切換到自定義鍵盤擴展,此時自定義鍵盤擴展中viewDidLoad方法中的數據庫配置代碼執行,但是控制檯出現錯誤提示:
  1. 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 { 
  2.  
  3.     reason = "Failed to create file; code = 2"
  4.  
上述錯誤表示在擴展的~/Library/Application%20Support/CustomKeyboardExt/demo.sqlite目錄創建.sqlite文件失敗。翻閱MagicalRecord源代碼(需要從github重新下載源代碼,.framework看不到源代碼),其中在創建.sqlite存儲文件路徑的代碼中會發現:
  1. + (NSURL *) MR_urlForStoreName:(NSString *)storeFileName { 
  2.  
  3. NSArray *paths = [NSArray arrayWithObjects:[self MR_applicationDocumentsDirectory], [self MR_applicationStorageDirectory], nil]; 
  4.  
  5.     NSFileManager *fm = [[NSFileManager alloc] init]; 
  6.  
  7.     for (NSString *path in paths) { 
  8.  
  9.         NSString *filepath = [path stringByAppendingPathComponent:storeFileName]; 
  10.  
  11.         if ([fm fileExistsAtPath:filepath]) { 
  12.  
  13.             return [NSURL fileURLWithPath:filepath]; 
  14.  
  15.         } 
  16.  
  17.     } 
  18.  
  19.     //set default url 
  20.  
  21.     return [NSURL fileURLWithPath:[[self MR_applicationStorageDirectory] stringByAppendingPathComponent:storeFileName]]; 
  22.  
其中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文件中的完整的代碼如下:
  1. #import "NSPersistentStore+Tracking.h" 
  2.  
  3. #import <objc/runtime.h> 
  4.  
  5. #import <MagicalRecord21/CoreData+MagicalRecord.h> 
  6.  
  7. static NSString * const kGroupName = @"group.com.aegeaon.ExtensionDemo"
  8.  
  9. static NSString * const kContainingDirectory = @"CoreDataStore/"
  10.  
  11. @implementation NSPersistentStore (Tracking) 
  12.  
  13. + (void)load { 
  14.  
  15.     static dispatch_once_t onceToken; 
  16.  
  17.     dispatch_once(&onceToken, ^{ 
  18.  
  19.         Class class = [self class]; 
  20.  
  21.         SEL originalSelector = @selector(MR_urlForStoreName:); 
  22.  
  23.         SEL swizzledSelector = @selector(ILS_urlForStoreName:); 
  24.  
  25.         SwizzleClassMethod(class, originalSelector, swizzledSelector); 
  26.  
  27.     }); 
  28.  
  29.  
  30. void SwizzleClassMethod(Class c, SEL orig, SEL new) { 
  31.  
  32.     Method origMethod = class_getClassMethod(c, orig); 
  33.  
  34.     Method newMethod = class_getClassMethod(c, new); 
  35.  
  36.     c = object_getClass((id)c); 
  37.  
  38.     if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) 
  39.  
  40.         class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); 
  41.  
  42.     else 
  43.  
  44.         method_exchangeImplementations(origMethod, newMethod); 
  45.  
  46.  
  47. #pragma mark - Method Swizzling 
  48.  
  49. + (NSURL *) ILS_urlForStoreName:(NSString *)storeFileName { 
  50.  
  51.     NSURL *storeURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:kGroupName]; 
  52.  
  53.     storeURL = [storeURL URLByAppendingPathComponent:[kContainingDirectory stringByAppendingString:storeFileName]]; 
  54.  
  55.     return storeURL; 
  56.  
  57.  
  58. @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和擴展都可以訪問的共享容器。
 
相關代碼如下:
  1. NSURLSession *mySession = [self configureMySession]; 
  2.  
  3. NSURL *url = [NSURL URLWithString:@"http://www.example.com/LargeFile.zip"]; 
  4.  
  5. NSURLSessionTask *myTask = [mySession downloadTaskWithURL:url]; 
  6.  
  7. [myTask resume]; 
  8.  
  9.  
  10. - (NSURLSession *) configureMySession { 
  11.  
  12.     if (!mySession) { 
  13.  
  14.         NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@“com.mycompany.myapp.backgroundsession”]; 
  15.  
  16.         config.sharedContainerIdentifier = @"com.mycompany.myappgroupidentifier"
  17.  
  18.         mySession = [NSURLSession sessionWithConfiguration:config delegate:selfdelegateQueue:nil]; 
  19.  
  20.     } 
  21.  
  22.     return mySession; 
  23.  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章