# 有關Share Extension 【iOS擴展開發攻略】

###最近接到的share Extension需求,要求作原生的share Extension,就去百度不少資料,坑坑窪窪的完成了任務上線,如今回過來總結一下踩過的坑.react

  • 一、首先給你們介紹一下iOS擴展.git

    擴展( Extension )是 iOS 8 中引入的一個很是重要的新特性。擴展讓 app 之間的數據交互成爲可能。用戶能夠在 app 中使用其餘應用提供的功能,而無需離開當前的應用。在 iOS 8 系統以前,每個 app 在物理上都是彼此獨立的, app 之間不能互訪彼此的私有數據。而在引入擴展以後,其餘 app 能夠與擴展進行數據交換。基於安全和性能的考慮,每個擴展運行在一個單獨的進程中,它擁有本身的 bundle , bundle 後綴名是.appex 。擴展 bundle 必須包含在一個普通應用的 bundle 的內部。github

    iOS 8 系統有 6 個支持擴展的系統區域,分別是 Today 、 Share 、 Action 、 Photo Editing 、 Storage Provider 、 Custom keyboard 。支持擴展的系統區域也被稱爲擴展點。 接下來咱們主要講一下share Extension具體實現。sql

  • 二、代碼實現 ####一、 share Extension實現是須要依賴一個工程,若是你沒有工程須要從新建立一個(若是你不會建立工程,你能夠關閉瀏覽器了);建立完工程後咱們須要建立一個share Extension的工程,建立方法如圖: vim

    image scr
    接着繼續點擊
    image
    image

    image

    完成後會如最後一張圖片,如今咱們就能夠嘗試一下share的功能。 數組

    image
    share Extension項目運行必須有一個容器,咱們先用默認的瀏覽器嘗試一下,
    image
    進入手機頁面你會發現share Extension按鈕是灰色狀態不能點擊,我先須要打開一個網頁,例如:(www.baidu.com),如今咱們點擊方向按鈕,會在share Extension欄找到咱們的APP

image

若是沒有發現APP,點擊旁邊更多按鈕,進入下級頁面,把APP權限按鈕打開 瀏覽器

image

####二、接下來咱們準備處理share Extension數據 安全

image
圖片爲蘋果原生爲咱們提供的share Extension頁面,進入程序shareViewController頁面
image
#####2.1依次來解析一下這三個方法 /* isContentValid來判斷咱們獲取到得數據是不是咱們想要的。 / - (BOOL)isContentValid { // Do validation of contentText and/or NSExtensionContext attachments here return YES; } /* * 點擊取消按鈕 */ - (void)didSelectCancel { [super didSelectCancel]; }

/**
 *  點擊提交按鈕
 */
- (void)didSelectPost
{
    // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.

    // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
    }
複製代碼

在這兩個方法裏面能夠進行一些自定義的操做。通常狀況下,當用戶點擊提交按鈕的時候,擴展要作的事情就是要把數據取出來,而且放入一個與Containing App(** 容器程序,儘管蘋果開放了Extension,可是在iOS中extension並不能單獨存在,要想提交到AppStore,必須將Extension包含在一個App中提交,而且App的實現部分不能爲空,這個包含Extension的App就叫Containing app。Extension會隨着Containing App的安裝而安裝,同時隨着ContainingApp的卸載而卸載。**)共享的數據介質中(包括NSUserDefault、Sqlite、CoreData),要跟容器程序進行數據交互須要藉助AppGroups服務,下面的章節會對這塊進行詳細說明。下面先來看看怎麼獲取擴展中的數據。app

#####2.2在ShareExtension中,UIViewController包含一個extensionContext這樣的上下文對象:dom

@interface UIViewController(NSExtensionAdditions) <NSExtensionRequestHandling>

// Returns the extension context. Also acts as a convenience method for a view controller to check if it participating in an extension request.
@property (nullable, nonatomic,readonly,strong) NSExtensionContext *extensionContext NS_AVAILABLE_IOS(8_0);
@end
複製代碼

經過操做它就能夠獲取到share Extension的數據,返回宿主應用的界面等操做。咱們能夠先看一下extensionContext的定義。

NS_CLASS_AVAILABLE(10_10, 8_0)
@interface NSExtensionContext : NSObject

// The list of input NSExtensionItems associated with the context. If the context has no input items, this array will be empty.
@property(readonly, copy, NS_NONATOMIC_IOSONLY) NSArray *inputItems;

// Signals the host to complete the app extension request with the supplied result items. The completion handler optionally contains any work which the extension may need to perform after the request has been completed, as a background-priority task. The `expired` parameter will be YES if the system decides to prematurely terminate a previous non-expiration invocation of the completionHandler. Note: calling this method will eventually dismiss the associated view controller.
- (void)completeRequestReturningItems:(nullable NSArray *)items completionHandler:(void(^ __nullable)(BOOL expired))completionHandler;

// Signals the host to cancel the app extension request, with the supplied error, which should be non-nil. The userInfo of the NSError will contain a key NSExtensionItemsAndErrorsKey which will have as its value a dictionary of NSExtensionItems and associated NSError instances.
- (void)cancelRequestWithError:(NSError *)error;

// Asks the host to open an URL on the extension's behalf
- (void)openURL:(NSURL *)URL completionHandler:(void (^ __nullable)(BOOL success))completionHandler;

@end

// Key in userInfo. Value is a dictionary of NSExtensionItems and associated NSError instances.
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionItemsAndErrorsKey NS_AVAILABLE(10_10, 8_0);

// The host process will enter the foreground
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostWillEnterForegroundNotification NS_AVAILABLE_IOS(8_2);

// The host process did enter the background
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostDidEnterBackgroundNotification NS_AVAILABLE_IOS(8_2);
複製代碼

#####2.3NSExtensionContext的結構比較簡單,包含一個屬性和三個方法。其說明以下:

方法 說明
inputItems 該數組存儲着容器應用傳入給NSExtensionContext的NSExtensionItem數組。其中每一個NSExtensionItem標識了一種類型的數據。要獲取數據就要從這個屬性入手。
completeRequestReturningItems:
completionHandler:
通知宿主程序的擴展已完成請求。調用此方法後,擴展UI會關閉並返回容器程序中。其中的items就是返回宿主程序的數據項。
cancelRequestWithError: 通知宿主程序的擴展已取消請求。調用此方法後,擴展UI會關閉並返回容器程序中。其中error爲錯誤的描述信息。
NSExtensionItemsAndErrorsKey NSExtensionItem的userInfo屬性中對應的錯誤信息鍵名。

#####2.4類的下面還定義了一些通知,這些通知都是跟宿主程序的行爲相關,在設計擴展的時候能夠根據這些通知來進行對應的操做。其說明以下:

通知名稱 說明
NSExtensionHostWillEnterForegroundNotification 宿主程序將要返回前臺通知NSExtensionHostDidEnterBackgroundNotification
NSExtensionHostWillResignActiveNotification 宿主程序將要被掛起通知
NSExtensionHostDidBecomeActiveNotification 宿主程序被激活通知

#####2.5從inputItems中獲取數據 inputItems是包含NSExtensionItem類型對象的數組。那麼,要處理裏面的數據還得先來了解一下NSExtensionItem的結構:

@interface NSExtensionItem : NSObject<NSCopying, NSSecureCoding>

// (optional) title for the item
@property(nullable, copy, NS_NONATOMIC_IOSONLY) NSAttributedString *attributedTitle;

// (optional) content text
@property(nullable, copy, NS_NONATOMIC_IOSONLY) NSAttributedString *attributedContentText;

// (optional) Contains images, videos, URLs, etc. This is not meant to be an array of alternate data formats/types, but instead a collection to include in a social media post for example. These items are always typed NSItemProvider.
@property(nullable, copy, NS_NONATOMIC_IOSONLY) NSArray *attachments;

// (optional) dictionary of key-value data. The key/value pairs accepted by the service are expected to be specified in the extension's Info.plist. The values of NSExtensionItem's properties will be reflected into the dictionary.
@property(nullable, copy, NS_NONATOMIC_IOSONLY) NSDictionary *userInfo;

@end

// Keys corresponding to properties exposed on the NSExtensionItem interface
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionItemAttributedTitleKey NS_AVAILABLE(10_10, 8_0);
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionItemAttributedContentTextKey NS_AVAILABLE(10_10, 8_0);
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionItemAttachmentsKey NS_AVAILABLE(10_10, 8_0);
複製代碼

NSExtensionItem包含四個屬性

屬性 說明
attributedTitle 標題
attributedContentText 內容。
attachments 附件數組,包含圖片、視頻、連接等資源,封裝在NSItemProvider類型中。
userInfo 一個key-value結構的數據。NSExtensionItem中的屬性都會在這個屬性中一一映射。

對應userInfo結構中的NSExtensionItem屬性的鍵名以下:

名稱 說明
NSExtensionItemAttributedTitleKey 標題的鍵名
NSExtensionItemAttributedContentTextKey 內容的鍵名。
NSExtensionItemAttachmentsKey 附件的鍵名

從上面的定義能夠看出除了文本內容,其餘類型的內容都是做爲附件存儲的,而附件又是封裝在一個叫NSItemProvider的類型中,其定義以下:

typedef void (^NSItemProviderCompletionHandler)(__nullable id <NSSecureCoding> item, NSError * __null_unspecified error);
typedef void (^NSItemProviderLoadHandler)(__null_unspecified NSItemProviderCompletionHandler completionHandler, __null_unspecified Class expectedValueClass, NSDictionary * __null_unspecified options);

// An NSItemProvider is a high level abstraction for file-like data objects supporting multiple representations and preview images.
NS_CLASS_AVAILABLE(10_10, 8_0)
@interface NSItemProvider : NSObject <NSCopying>

// Initialize an NSItemProvider with a single handler for the given item.
- (instancetype)initWithItem:(nullable id <NSSecureCoding>)item typeIdentifier:(nullable NSString *)typeIdentifier NS_DESIGNATED_INITIALIZER;

// Initialize an NSItemProvider with load handlers for the given file URL, and the file content.
- (nullable instancetype)initWithContentsOfURL:(null_unspecified NSURL *)fileURL;

// Sets a load handler block for a specific type identifier. Handlers are invoked on demand through loadItemForTypeIdentifier:options:completionHandler:. To complete loading, the implementation has to call the given completionHandler. Both expectedValueClass and options parameters are derived from the completionHandler block.
- (void)registerItemForTypeIdentifier:(NSString *)typeIdentifier loadHandler:(NSItemProviderLoadHandler)loadHandler;

// Returns the list of registered type identifiers
@property(copy, readonly, NS_NONATOMIC_IOSONLY) NSArray *registeredTypeIdentifiers;

// Returns YES if the item provider has at least one item that conforms to the supplied type identifier.
- (BOOL)hasItemConformingToTypeIdentifier:(NSString *)typeIdentifier;

// Loads the best matching item for a type identifier. The client's expected value class is automatically derived from the blocks item parameter. Returns an error if the returned item class does not match the expected value class. Item providers will perform simple type coercions (eg. NSURL to NSData, NSURL to NSFileWrapper, NSData to UIImage).
- (void)loadItemForTypeIdentifier:(NSString *)typeIdentifier options:(nullable NSDictionary *)options completionHandler:(nullable NSItemProviderCompletionHandler)completionHandler;

@end

// Common keys for the item provider options dictionary.
FOUNDATION_EXTERN NSString * __null_unspecified const NSItemProviderPreferredImageSizeKey NS_AVAILABLE(10_10, 8_0); // NSValue of CGSize or NSSize, specifies image size in pixels.

@interface NSItemProvider(NSPreviewSupport)

// Sets a custom preview image handler block for this item provider. The returned item should preferably be NSData or a file NSURL.
@property(nullable, copy, NS_NONATOMIC_IOSONLY) NSItemProviderLoadHandler previewImageHandler NS_AVAILABLE(10_10, 8_0);

// Loads the preview image for this item by either calling the supplied preview block or falling back to a QuickLook-based handler. This method, like loadItemForTypeIdentifier:options:completionHandler:, supports implicit type coercion for the item parameter of the completion block. Allowed value classes are: NSData, NSURL, UIImage/NSImage.
- (void)loadPreviewImageWithOptions:(null_unspecified NSDictionary *)options completionHandler:(null_unspecified NSItemProviderCompletionHandler)completionHandler NS_AVAILABLE(10_10, 8_0);

@end

// Keys used in property list items received from or sent to JavaScript code

// If JavaScript code passes an object to its completionFunction, it will be placed into an item of type kUTTypePropertyList, containing an NSDictionary, under this key.
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionJavaScriptPreprocessingResultsKey NS_AVAILABLE(10_10, 8_0);

// Arguments to be passed to a JavaScript finalize method should be placed in an item of type kUTTypePropertyList, containing an NSDictionary, under this key.
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionJavaScriptFinalizeArgumentKey NS_AVAILABLE_IOS(8_0);

// Errors

// Constant used by NSError to distinguish errors belonging to the NSItemProvider domain
FOUNDATION_EXTERN NSString * __null_unspecified const NSItemProviderErrorDomain NS_AVAILABLE(10_10, 8_0);

// NSItemProvider-related error codes
typedef NS_ENUM(NSInteger, NSItemProviderErrorCode) {
    NSItemProviderUnknownError                                      = -1,
    NSItemProviderItemUnavailableError                              = -1000,
    NSItemProviderUnexpectedValueClassError                         = -1100,
    NSItemProviderUnavailableCoercionError NS_AVAILABLE(10_11, 9_0) = -1200
} NS_ENUM_AVAILABLE(10_10, 8_0);
複製代碼

NSItemProvider結構說明

名稱 說明
initWithItem:typeIdentifier: 初始化方法,item爲附件的數據,typeIdentifier是附件對應的類型標識,對應UTI的描述。
initWithContentsOfURL 根據制定的文件路徑來初始化。
registerItemForTypeIdentifier:loadHandler: 爲一種資源類型自定義加載過程。這個方法主要針對自定義資源使用,例如本身定義的類或者文件格式等。當調用loadItemForTypeIdentifier:options:completionHandler:方法時就會觸發定義的加載過程。
hasItemConformingToTypeIdentifier: 用於判斷是否有typeIdentifier(UTI)所指定的資源存在。存在則返回YES,不然返回NO。
該方法結合loadItemForTypeIdentifier:options:completionHandler:使用。
loadItemForTypeIdentifier:options:completionHandler: 加載typeIdentifier指定的資源。加載是一個異步過程,加載完成後會觸發completionHandler。
loadPreviewImageWithOptions:completionHandler: 加載資源的預覽圖片。

因而可知,其結構以下圖所示:

image

爲了要取到宿主程序提供的數組,那麼只要關注loadItemTypeIdentifier:options:completionHandler方法的使用便可。有了上面的瞭解,那麼接下來就是對inputItems進行數據分析並提取了,這裏以一個連接的share Extension爲例,改寫視圖控制器中的didSelectPost方法。看下面的代碼:

- (void)didSelectPost
{
    __block BOOL hasExistsUrl = NO;
    [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem * _Nonnull extItem, NSUInteger idx, BOOL * _Nonnull stop) {
        [item.attachments enumerateObjectsUsingBlock:^(NSItemProvider * _Nonnull itemProvider, NSUInteger idx, BOOL * _Nonnull stop) {
         //獲取圖片
			if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"])
			            {
			                [itemProvider loadItemForTypeIdentifier:@"public.url"
			                                                options:nil
			                                      completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {
			
			                                          if ([(NSObject *)item isKindOfClass:[NSURL class]])
			                                          {
			                                              NSLog(@"share Extension的URL = %@", item);
			                                          }
			
			                                      }];
			
			                hasExistsUrl = YES;
			                *stop = YES;
			            }
			
			        }];
          //獲取連接
            if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"])
            {
                [itemProvider loadItemForTypeIdentifier:@"public.url"
                                                options:nil
                                      completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {

                                          if ([(NSObject *)item isKindOfClass:[NSURL class]])
                                          {
                                              NSLog(@"share Extension的URL = %@", item);
                                          }

                                      }];

                hasExistsUrl = YES;
                *stop = YES;
            }

        }];

        if (hasExistsUrl)
        {
            *stop = YES;
        }

    }];

    // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
    // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
//    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
}
複製代碼

上面的例子中遍歷了extensionContext的inputItems數組中全部NSExtensionItem對象,而後從這些對象中遍歷attachments數組中的全部NSItemProvider對象。匹配第一個包含public.url標識的附件(具體要匹配什麼資源,數量是多少皆有本身的業務所決定)。**** 注意:在上面代碼中註釋了*[self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];這行代碼,主要是使到視圖控制器不被關閉,等到實現相應的處理後再進行調用該方法,對share Extension視圖進行關閉。*** 在下面的章節會說明這一點。

####2.5 將share Extension數據傳遞給容器程序

上面章節已經講述瞭如何取得宿主應用所share Extension的內容。那麼,接下來就是將這些內容傳遞給容器程序進行相應的操做(如:在一款社交應用中,可能會爲取得的share Extension內容發佈一條用戶動態)。在默認狀況下,iOS的應用是存在一個沙盒裏面的,不容許應用與應用直接進行數據的交互。爲此,蘋果提供了一項叫App Groups的服務,該服務容許開發者能夠在本身的應用之間經過NSUserDefaults、NSFileManager或者CoreData來進行相互的數據傳輸。下面介紹如何激活App Groups服務:

首先要有一個獨立的AppID(帶通配符*號的AppID是不容許激活App Groups的)

==Xcode中直接打開group,設置group後,開發者網站會同步**==

image

點擊添加按鈕,會出現添加框

[圖片上傳失敗...(image-bf122-1526268288844)]

gronp.後面填寫你項目的bundle identifer 便可;一樣在share 項目中添加group信息,(系統應該已經默認爲你添加上,默認選擇就好)

image

至此,應用和擴展的App Groups服務都已經啓動,如今就要進行share Extension內容的傳輸操做。下面分別介紹一下NSUserDefaults、NSFileManager以及CoreData三種方式是如何實現App Groups下的數據操做:

  • NSUserDefaults:要想設置或訪問Group的數據,不能在使用standardUserDefaults方法來獲取一個NSUserDefaults對象了。應該使用initWithSuiteName:方法來初始化一個NSUserDefaults對象,其中的SuiteName就是建立的Group的名字,而後利用這個對象來實現,跨應用的數據讀寫,代碼以下:

//初始化一個供App Groups使用的NSUserDefaults對象

NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.Taiyi.shareP"];

//寫入數據

[userDefaults setValue:@"value" forKey:@"key"];

//讀取數據

NSLog(@"%@", [userDefaults valueForKey:@"key"]);
複製代碼
  • NSFileManager:經過調用 containerURLForSecurityApplicationGroupIdentifier:方法能夠得到AppGroup的共享目錄,而後在此目錄的基礎上實現任意的文件操做。代碼以下:

    //獲取分組的共享目錄
    
    NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cn.vimfung.ShareExtensionDemo"];
    
    NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"demo.txt"];
    
    //寫入文件
    
    [@"abc" writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:nil];
    
    //讀取文件
    
    NSString *str = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:nil];
    
    NSLog(@"str = %@", str);
    複製代碼
  • CoreData:其實CoreData是基於NSFileManager取得共享目錄後來實現數據共享的。即在初始化CoreData時,先使用NSFileManager取得共享目錄,而後再指定共享目錄爲存儲數據文件的目錄(如存儲的sqlite文件)。代碼以下:

    //獲取分組的共享項目
      NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cn.vimfung.ShareExtensionDemo"];
      NSURL *storeURL = [containerURL URLByAppendingPathComponent:@"DataModel.sqlite"];
      
      //初始化持久化存儲調度器
      NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"DataModel" withExtension:@"momd"];
      
      NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
      NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
      
      [coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                configuration:nil
                                          URL:storeURL
                                      options:nil
                                        error:nil];
      
      //建立受控對象上下文
      NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
      
      [context performBlockAndWait:^{
          [context setPersistentStoreCoordinator:coordinator];
    複製代碼

    }]; 爲了方便演示,這裏會使用NSUserDefault來直接把取到的url地址保存起來。代碼以下所示:

_(默認狀況下,若是用戶點擊Post按鈕後,share Extension界面就會消失,用戶能夠繼續對宿主程序進行操做。這些都要靠NSExtensionContextdcompleteRequestReturningItems:completionHandler:方法來實現。如今,因爲在didSelectPost方法中加入了share Extension內容的處理,因爲獲取附件是一個異步過程,那麼,就須要作好界面上的提示。不然,share Extension界面消失後因爲沒有操做提示,會使用戶誤覺得界面進行卡死的狀態,實際上是share Extension內容尚未處理完成。接下來就是優化UI上的提示操做, )_

- (void)didSelectPost {
    
    _viewCon = [ViewController new];
    NSString *aa = [_viewCon setNumber];
    NSLog(@"%@",aa);
    //加載動畫初始化
    UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
    activityIndicatorView.frame = CGRectMake((self.view.frame.size.width - activityIndicatorView.frame.size.width) / 2,
                                             (self.view.frame.size.height - activityIndicatorView.frame.size.height) / 2,
                                             activityIndicatorView.frame.size.width,
                                             activityIndicatorView.frame.size.height);
    activityIndicatorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
    [self.view addSubview:activityIndicatorView];
    
    //激活加載動畫
    [activityIndicatorView startAnimating];

    // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
    
    __block BOOL hasExistsUrl = NO;
    [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem * _Nonnull extItem, NSUInteger idx, BOOL * _Nonnull stop) {
    NSLog(@"%@-----------%@",extItem.attributedTitle,extItem.attributedContentText);
    
    NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.taiyi.shareP"];
    NSAttributedString *strings = [extItem.attributedContentText attributedSubstringFromRange:NSMakeRange(0, extItem.attributedContentText.length)];
    NSArray *array = [strings.string componentsSeparatedByString:@"\n"];
    NSString *firstString = array[0];
    NSLog(@"%@",firstString);
    [userDefaults setValue:firstString forKey:@"share-content"];
   
    [extItem.attachments enumerateObjectsUsingBlock:^(NSItemProvider * _Nonnull itemProvider, NSUInteger idx, BOOL * _Nonnull stop) {

        NSLog(@"%d",[itemProvider hasItemConformingToTypeIdentifier:@"public.url"]);
        
        if ([itemProvider hasItemConformingToTypeIdentifier:@"public.text"])
        {
            //加載typeIdentifier指定的資源
            [itemProvider loadItemForTypeIdentifier:@"public.text"
                                            options:nil
                                  completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {
                                      
                                      if ([(NSObject *)item isKindOfClass:[NSURL class]])
                                          
                                      {
                                          NSLog(@"share Extension的URL = %@", item);
                                          
                                          [userDefaults setValue:((NSURL *)item).absoluteString forKey:@"share-text-url"];
                                          
                                          //用於標記是新的share Extension
                                          [userDefaults setBool:YES forKey:@"has-new-share"];
                                          
                                          [activityIndicatorView stopAnimating];
                                          [self.extensionContext completeRequestReturningItems:@[extItem] completionHandler:nil];
                                          
                                      }
                                      
                                  }];
            
            hasExistsUrl = YES;
            *stop = YES;
        }
        
        if ([itemProvider hasItemConformingToTypeIdentifier:@"public.image"])
        {
            //加載typeIdentifier指定的資源
            [itemProvider loadItemForTypeIdentifier:@"public.image"
                                            options:nil
                                  completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {
                                      
                                      if ([(NSObject *)item isKindOfClass:[NSURL class]])
                                          
                                      {
                                          NSLog(@"share Extension的URL = %@", item);
                                          
                                          [userDefaults setValue:((NSURL *)item).absoluteString forKey:@"share-image-url"];
                                          
                                          //用於標記是新的share Extension
                                          [userDefaults setBool:YES forKey:@"has-new-share"];
                                          
                                          [activityIndicatorView stopAnimating];
                                          [self.extensionContext completeRequestReturningItems:@[extItem] completionHandler:nil];
                                          
                                      }
                                      
                                  }];
            
            hasExistsUrl = YES;
            *stop = YES;
        }

        
        
        if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"])
        {
            //加載typeIdentifier指定的資源
            [itemProvider loadItemForTypeIdentifier:@"public.url"
                                            options:nil
                                  completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {
                                      
                                      if ([(NSObject *)item isKindOfClass:[NSURL class]])

                                      {
                                          NSLog(@"share Extension的URL = %@", item);
                                          
                                          [userDefaults setValue:((NSURL *)item).absoluteString forKey:@"share-url"];
                                         
                                          //用於標記是新的share Extension
                                          [userDefaults setBool:YES forKey:@"has-new-share"];
                                          
                                          [activityIndicatorView stopAnimating];
                                          [self.extensionContext completeRequestReturningItems:@[extItem] completionHandler:nil];
                                          
                                      }
                                      
                                  }];
            
            hasExistsUrl = YES;
            *stop = YES;
        }
        
    }];
    
    if (hasExistsUrl)
    {
        *stop = YES;
    }
    
}];

if (!hasExistsUrl)
{
    //直接退出
    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
}
}
複製代碼

####2.6 容器程序獲取share Extension數據

插件的工做基本上已經所有開發完成了,接下來就是容器程序獲取數據並進行操做。下面是容器程序的處理代碼:

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    //獲取共享的UserDefaults
    NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];
    if ([userDefaults boolForKey:@"has-new-share"])
    {
        NSLog(@"新的share Extension : %@", [userDefaults valueForKey:@"share-url"]);

        //重置share Extension標識
        [userDefaults setBool:NO forKey:@"has-new-share"];
    }
}
複製代碼

爲了方便演示,這裏直接在AppDelegate中的applicationDidBecomeActive:方法中檢測是否有新的share Extension,若是有則經過Log打印連接出來。

####2.7 在share share Extension中,應該會有不少同窗由於數據傳輸,頁面調用問題發愁,接下來重點介紹一下我使用的方法==直接喚起APP,完成 share Extension==

* 咱們須要給APP配置一個url Schemes;

image

  • 而後在shareViewController裏面調用

    __block BOOL hasExistsUrl = NO;
      [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem * _Nonnull extItem, NSUInteger idx, BOOL * _Nonnull stop) {
          NSLog(@"%@-----------%@",extItem.attributedTitle,extItem.attributedContentText);
          NSAttributedString *strings = [extItem.attributedContentText attributedSubstringFromRange:NSMakeRange(0, extItem.attributedContentText.length)];
      
      NSArray *array = [strings.string componentsSeparatedByString:@"\n"];
      self.titleString = [NSString stringWithFormat:@"%@",array[0]];
      
      [extItem.attachments enumerateObjectsUsingBlock:^(NSItemProvider * _Nonnull itemProvider, NSUInteger idx, BOOL * _Nonnull stop) {
          //用於判斷是否有typeIdentifier(UTI)所指定的資源存在。
          if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"])
          {
              //加載typeIdentifier指定的資源
              [itemProvider loadItemForTypeIdentifier:@"public.url"
                                              options:nil
                                    completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {
                                        
                                        if ([(NSObject *)item isKindOfClass:[NSURL class]])
                                        {
                                            NSLog(@"分享的URL = %@", item);
                                            self.urlString = [NSString stringWithFormat:@"%@",item];
    
    
                                            NSString *urlStr = [NSString stringWithFormat:@"shareP://?articleTitle=%@&articleUrl=%@",[self encode:self.titleString], [self encode:self.urlString]];
    
                                            if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:urlStr]]) {
                                                //能夠調起APP
                                                [[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlStr]];
                                                NSLog(@"調起成功");
                                                
                                                //直接退出
                                                [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
                                            }
                                            
                                        }
                                        
                                    }];
              
              hasExistsUrl = YES;
              *stop = YES;
          }
          
      }];
      
      if (hasExistsUrl)
      {
          *stop = YES;
      }
           }];
      
      if (!hasExistsUrl)
      {
          //直接退出
          [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
      }
      [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userHasebeenLocation) name:@"dismissController" object:nil];
    複製代碼
  • 在項目appDelegate中接收信息

    //受權登陸操做 -(BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { NSString *urlStr = url.absoluteString; NSString *sechemes = url.scheme; if([[self getAppSchemeString] isEqualToString:sechemes]){

    if ([urlStr containsString:@"articleTitle"] && [urlStr containsString:@"articleUrl"]) {
          NSRange range1 = [urlStr rangeOfString:@"="];
          NSRange range2 = [urlStr rangeOfString:@"&"];
          NSString *articleTitle = [urlStr substringWithRange:NSMakeRange(range1.location + range1.length, range2.location - range1.location - range1.length)];
          
          NSString *stateStr = [urlStr substringFromIndex:range2.location+1];
          NSRange range3 = [stateStr rangeOfString:@"="];
          NSString *articleUrl = [stateStr substringFromIndex:range3.location+1];
          NSLog(@"%@====%@",articleTitle,articleUrl);
          
          
    
              //跳轉代碼就是跳轉到你本身設計的控制器,我這裏就不寫了
      }
      
      }
      return YES;
      }
    複製代碼

到此,share Extension 調用APP完成,剩下的所有均可以在APP內部操做了,是否是很方便,

####2.7配置info文件

group設置完成後,咱們須要配置修改info文件中的NSExtensionActivationRule字段

image

咱們只須要關注如下幾個字段的設置:

名稱 說明
Bundle display name 擴展的顯示名稱,默認跟你的項目名稱相同,能夠經過修改此字段來控制擴展的顯示名稱。
NSExtension 擴展描述字段,用於描述擴展的屬性、設置等。做爲一個擴展項目必需要包含此字段。
NSExtensionAttributes 擴展屬性集合字段。用於描述擴展的屬性。
NSExtensionActivationRule 激活擴展的規則。默認爲字符串「TRUEPREDICATE」,表示在share Extension菜單中一直顯示該擴展。能夠將類型改成Dictionary類型,而後添加如下字段:
NSExtensionActivationSupportsAttachmentsWithMaxCount
NSExtensionActivationSupportsAttachmentsWithMinCount
NSExtensionActivationSupportsImageWithMaxCount
NSExtensionActivationSupportsMovieWithMaxCount
NSExtensionActivationSupportsWebPageWithMaxCount
NSExtensionActivationSupportsWebURLWithMaxCount
NSExtensionMainStoryboard 設置主界面的Storyboard,若是不想使用storyboard,也可使用NSExtensionPrincipalClass指定自定義UIViewController子類名
NSExtensionPointIdentifier 擴展標識,在share Extension擴展中爲:com.apple.share-services
NSExtensionPrincipalClass 自定義UI的類名
NSExtensionActivationSupportsAttachmentsWithMaxCount 附件最多限制,爲數值類型。附件包括File、Image和Movie三大類,單1、混選總量不超過指定數量
NSExtensionActivationSupportsAttachmentsWithMinCount 附件最少限制,爲數值類型。當設置NSExtensionActivationSupportsAttachmentsWithMaxCount時生效,默認至少選擇1個附件,share Extension菜單中才顯示擴展插件圖標。
NSExtensionActivationSupportsFileWithMaxCount 文件最多限制,爲數值類型。文件泛指除Image/Movie以外的附件,例如【郵件】附件、【語音備忘錄】等。

單1、混選均不超過指定數量。
NSExtensionActivationSupportsImageWithMaxCount 圖片最多限制,爲數值類型。單1、混選均不超過指定數量
NSExtensionActivationSupportsMovieWithMaxCount 視頻最多限制,爲數值類型。單1、混選均不超過指定數量。
NSExtensionActivationSupportsText 是否支持文本類型,布爾類型,默認不支持。如【備忘錄】的share Extension
NSExtensionActivationSupportsWebURLWithMaxCount Web連接最多限制,爲數值類型。默認不支持share Extension超連接,須要本身設置一個數值。
NSExtensionActivationSupportsWebPageWithMaxCount Web頁面最多限制,爲數值類型。默認不支持Web頁面share Extension,須要本身設置一個數值。

對於不一樣的應用裏面有可能出現只容許接受某種類型的內容,那麼Share Extension就不能一直出如今share Extension菜單中,由於不一樣的應用提供的share Extension內容不同,這就須要經過設置NSExtensionActivationRule字段來決定Share Extension是否顯示。例如,只想接受其餘應用share Extension連接到本身的應用,那麼能夠經過下面的步驟來設置:

將NSExtensionActivationRule字段類型由String改成Dictionary。

展開NSExtensionActivationRule字段,建立其子項NSExtensionActivationSupportsWebURLWithMaxCount,並設置一個限制數量。

==必定要把所有的規則配置,不然對應的share Extension中不會顯示APP==

#####3 提審AppStore的注意事項

  • 擴展中的處理不能太長時間阻塞主線程(建議放入線程中到處理),不然可能致使蘋果拒絕你的應用。

  • 擴展不能單獨提審,必需要跟容器程序一塊兒提交AppStore進行審覈。

  • 提審的擴展和容器程序的Build Version要保持一致,不然在上傳審覈包的時候會提示警告,致使程序沒法正常提審。

  • 若是你的APP要送審APPStore必須所有配置NSExtensionActivationRule,字段類型必須對應,不然會提交失敗,

  • 若是你的APP是用企業帳號分發,==強烈建議不要使用group方式傳遞數據==,企業帳號分發會關閉group,致使不能數據傳輸,

####4. 進階研究

  • 4.1 對默認分享界面進行擴展

在某些狀況下,在分享界面中會加入一下其它信息的顯示,或者其它的選項供用戶操做。如:內容要分享給什麼好友、分享內容的可見權限等等。那麼,默認的分享界面( SLComposeServiceViewController)提供了相關的方法來對其進行擴展。這些方法定義以下

#if TARGET_OS_IPHONE
	/*
	 Configuration Item Support (account pickers, privacy selection, location, etc.)
	 */
		
	// Subclasses should implement this, and return an array of SLComposeSheetConfigurationItem instances, if if needs to display configuration items in the sheet. Defaults to nil.
	- (NSArray *)configurationItems;
	
	// Forces a reload of the configuration items table.
	// This is typically only necessary for subclasses that determine their configuration items in a deferred manner (for example, in -presentationAnimationDidFinish).
	// You do not need to call this after changing a configuration item property; the base class detects and reacts to that automatically.
	- (void)reloadConfigurationItems;
	
	// Presents a configuration view controller. Typically called from a configuration item's tapHandler. Only one configuration view controller is allowed at a time.
	// The pushed view controller should set preferredContentSize appropriately. SLComposeServiceViewController observes changes to that property and animates sheet size changes as necessary.
	- (void)pushConfigurationViewController:(UIViewController *)viewController;
	
	// Dismisses the current configuration view controller.
	- (void)popConfigurationViewController;
	#endif
複製代碼

其屬性說明以下:

屬性 說明
title 配置項標題
value 當前的配置值
valuePending YES時,顯示值位置顯示加載動畫,NO時,顯示配置的值。
tapHandler 點擊配置項的事件處理

下面將經過使用這些方法來擴展UI,使插件增長兩個配置項:一個是是否公開分享的配置項,該選項標識一個開關值。另一個是公開權限設置項,在是否公開分享的開關爲開時顯示。能夠選擇分享給全部人仍是好友。代碼以下所示:

- (NSArray *)configurationItems {
    // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.

    //定義兩個配置項,分別記錄用戶選擇是否公開以及公開的權限,而後根據配置的值
    static BOOL isPublic = NO;
    static NSInteger act = 0;

    NSMutableArray *items = [NSMutableArray array];

    //建立是否公開配置項
    SLComposeSheetConfigurationItem *item = [[SLComposeSheetConfigurationItem alloc] init];
    item.title = @"是否公開";
    item.value = isPublic ? @"是" : @"否";

    __weak ShareViewController *theController = self;
    __weak SLComposeSheetConfigurationItem *theItem = item;
    item.tapHandler = ^{

        isPublic = !isPublic;
        theItem.value = isPublic ? @"是" : @"否";


        [theController reloadConfigurationItems];
    };

    [items addObject:item];

    if (isPublic)
    {
        //若是公開標識爲YES,則建立公開權限配置項
        SLComposeSheetConfigurationItem *actItem = [[SLComposeSheetConfigurationItem alloc] init];

        actItem.title = @"公開權限";

        switch (act)
        {
            case 0:
                actItem.value = @"全部人";
                break;
            case 1:
                actItem.value = @"好友";
                break;
            default:
                break;
        }

        actItem.tapHandler = ^{

            //設置分享權限時彈出選擇界面
            ShareActViewController *actVC = [[ShareActViewController alloc] init];
            [theController pushConfigurationViewController:actVC];

            [actVC onSelected:^(NSIndexPath *indexPath) {

                //當選擇完成時退出選擇界面並刷新配置項。
                act = indexPath.row;
                [theController popConfigurationViewController];
                [theController reloadConfigurationItems];

            }];

        };

        [items addObject:actItem];
    }

    return items;
	}
複製代碼

ShareActViewController 的實現

@interface ShareActViewController () <UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) void (^selectedHandler) ();

@end

@implementation ShareActViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
    tableView.backgroundColor = [UIColor clearColor];
    tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    tableView.dataSource = self;
    tableView.delegate = self;
    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
    [self.view addSubview:tableView];
}

- (void)onSelected:(void(^)(NSIndexPath *indexPath))handler
{
    self.selectedHandler = handler;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 2;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    cell.backgroundColor = [UIColor clearColor];

    switch (indexPath.row)
    {
        case 0:
            cell.textLabel.text = @"全部人";
            break;
        case 1:
            cell.textLabel.text = @"好友";
            break;
        default:
            break;
    }

    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.selectedHandler)
    {
        self.selectedHandler (indexPath);
    }
}
複製代碼

在分享插件界面中重寫了configurationItems方法,而後定義了兩個配置項屬性,分別是是否公開標識isPublic和公開權限act。而後建立是否公開的SLComposeSheetConfigurationItem配置項和根據isPublic的值來判斷是否建立公開權限配置項。其中是否公開配置點擊時會變動isPublic的值,從而達到顯示或隱藏公開權限配置。而公開權限配置的點擊則彈出一個選擇的TableView,用於選擇給定的值而後返回到分享界面。

####5. 替換Share Extension中的默認分享界面

一、若是經過擴展SLComposeServiceViewController還不能知足需求的狀況下,這時候就須要本身設計一個分享視圖控制器來替換默認的SLComposeServiceViewController。

首先,建立一個自定義視圖控制器,如:CustomShareViewController。

二、而後打開擴展的Info.plist文件,刪除NSExtensionMainStoryboard屬性並增長一項NSExtensionPrincipalClass屬性並指向CustomShareViewController(注:這裏沒有使用Storyboard因此要刪除該屬性),如圖:

image

三、接下來根據實際的須要來設計分享視圖的展現與交互形式。

四、而後調用CustomShareViewController的extensionContext屬性來控制擴展的提交與取消等操做(注:因爲擴展中導入了關於ExtensionContext的UIViewController類目,所以,每一個ViewController都帶有extensionContext屬性)。

爲了演示的簡單性,下面的代碼會經過extensionContext獲取到url後,給到自定義分享視圖的Label中顯示,同時也提供一個提交和取消按鈕,用於用戶對分享內容的操做。代碼以下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    //定義一個容器視圖來存放分享內容和兩個操做按鈕
    UIView *container = [[UIView alloc] initWithFrame:CGRectMake((self.view.frame.size.width - 300) / 2, (self.view.frame.size.height - 175) / 2, 300, 175)];
    container.layer.cornerRadius = 7;
    container.layer.borderColor = [UIColor lightGrayColor].CGColor;
    container.layer.borderWidth = 1;
    container.layer.masksToBounds = YES;
    container.backgroundColor = [UIColor whiteColor];
    container.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
    [self.view addSubview:container];

    //定義Post和Cancel按鈕
    UIButton *cancelBtn = [UIButton buttonWithType:UIButtonTypeSystem];
    [cancelBtn setTitle:@"Cancel" forState:UIControlStateNormal];
    cancelBtn.frame = CGRectMake(8, 8, 65, 40);
    [cancelBtn addTarget:self action:@selector(cancelBtnClickHandler:) forControlEvents:UIControlEventTouchUpInside];
    [container addSubview:cancelBtn];

    UIButton *postBtn = [UIButton buttonWithType:UIButtonTypeSystem];
    [postBtn setTitle:@"Post" forState:UIControlStateNormal];
    postBtn.frame = CGRectMake(container.frame.size.width - 8 - 65, 8, 65, 40);
    [postBtn addTarget:self action:@selector(postBtnClickHandler:) forControlEvents:UIControlEventTouchUpInside];
    [container addSubview:postBtn];

    //定義一個分享連接標籤
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(8,
                                                               cancelBtn.frame.origin.y + cancelBtn.frame.size.height + 8,
                                                               container.frame.size.width - 16,
                                                               container.frame.size.height - 16 - cancelBtn.frame.origin.y - cancelBtn.frame.size.height)];
    label.numberOfLines = 0;
    label.textAlignment = NSTextAlignmentCenter;
    [container addSubview:label];

    //獲取分享連接
    __block BOOL hasGetUrl = NO;
    [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem *  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

        [obj.attachments enumerateObjectsUsingBlock:^(NSItemProvider *  _Nonnull itemProvider, NSUInteger idx, BOOL * _Nonnull stop) {

            if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"])
            {
                [itemProvider loadItemForTypeIdentifier:@"public.url" options:nil completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {

                    if ([(NSObject *)item isKindOfClass:[NSURL class]])
                    {
                        dispatch_async(dispatch_get_main_queue(), ^{

                            label.text = ((NSURL *)item).absoluteString;

                        });
                    }

                }];

                hasGetUrl = YES;
                *stop = YES;
            }

            *stop = hasGetUrl;

        }];

    }];
}

- (void)cancelBtnClickHandler:(id)sender
{
    //取消分享
    [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"CustomShareError" code:NSUserCancelledError userInfo:nil]];
}

- (void)postBtnClickHandler:(id)sender
{
    //執行分享內容處理
    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
}
複製代碼

share Extension 的基本內容就是這樣了,

下面是Demo的地址;[shareP](github.com/wang22290/s…)

相關文章
相關標籤/搜索