iOS - Share Extension

1. 學 Share Extension 以前 先了解一下iOS的App Extension

2.1 建立Share Extension擴展Target

** 注:擴展不能單首創建,必須依賴於應用工程項目,所以若是你尚未建立一個應用工程,先去建立一個。**html

一、打開項目設置,在TARGETS側欄地下點擊「+」號來建立一個新的Target,如圖:react

 
添加Target

二、而後選擇」iOS」 -> 「Application Extension」 -> 「Share Extension」,點擊「Next」。如圖:ios

 
建立Share擴展

三、給擴展起個名字,這裏填寫了「Share」,點擊「Finish」。如圖:sql

 
填寫擴展信息

四、這時候會提示建立一個Scheme,點擊「Activate」。如圖:vim

 
 

那麼,直到這裏建立Share Extension的工做就算是完成了。接下來能夠先進行一下編譯運行。這裏跟作App開發的時候會稍微有點不同。由於Extension是須要Host App(宿主應用)來運行的。因此,XCode中會彈出界面讓咱們選擇一個iOS的App來運行Extension。如圖:數組

 
選擇宿主應用

這裏我選擇了XCode建議的應用Safari,而後點擊「Run」來進行調試運行。XCode會啓動Safari,如圖:session

 
 

能看到Safari中間的分享按鈕是灰色不可用的。別急,你還沒打開一個網頁呢_。咱們隨便點開一個網頁,能夠看到分享按鈕變爲激活狀態。點擊分享按鈕就會彈出分享菜單,如圖:app

 
運行效果圖

能夠看到剛纔創建的Share擴展已經顯示在面板上了,若是你沒有發現本身的擴展,那麼你能夠將菜單滑動到最右邊,在「更多」選項中激活本身的擴展。如圖:dom

 
 

咱們點擊本身建立的分享項,其彈出一個分享窗口。如圖:異步

 
分享界面效果圖

2.2. 配置Share Extension

接下來咱們須要給他一些設置。咱們展開XCode左側欄的Share目錄,找到Info.plist文件。如:

 
擴展Info.plist

咱們只須要關注如下幾個字段的設置:(更多詳細的Information Property List Key Reference

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

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

  1. 將NSExtensionActivationRule字段類型由String改成Dictionary。
  2. 展開NSExtensionActivationRule字段,建立其子項NSExtensionActivationSupportsWebURLWithMaxCount,並設置一個限制數量。

調整後以下圖所示:

 
Info.plist

2.3 處理Share Extension中的數據

其實在Share Extension中默認都會有一個數據展示的UI界面。該界面繼承SLComposeServiceViewController這個類型,如:

@interface ShareViewController : SLComposeServiceViewController

@end

 

其展示效果,如圖:
 
分享界面

頂部包括了標題、取消(Cancel)按鈕和提交(Post)按鈕。而後下面跟着左邊就是一個文本編輯框,右邊就是一個圖片顯示控件。那麼,每當用戶點擊取消按鈕或者提交按鈕時,都會分別觸發下面的方法:

/**
 *  點擊取消按鈕
 */
- (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服務,下面的章節會對這塊進行詳細說明。下面先來看看怎麼獲取擴展中的數據。

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

@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

 

經過操做它就能夠獲取到分享的數據,返回宿主應用的界面等操做。咱們能夠先看一下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);

// The host process will resign active status (stop receiving events), the extension may be suspended
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostWillResignActiveNotification NS_AVAILABLE_IOS(8_2);

// The host process did become active (begin receiving events)
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostDidBecomeActiveNotification NS_AVAILABLE_IOS(8_2);

 

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

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

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

2.3.1 從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。<br />該方法結合loadItemForTypeIdentifier:options:completionHandler:使用。
loadItemForTypeIdentifier:options:completionHandler: 加載typeIdentifier指定的資源。加載是一個異步過程,加載完成後會觸發completionHandler。
loadPreviewImageWithOptions:completionHandler: 加載資源的預覽圖片。

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

 
層次結構圖

爲了要取到宿主程序提供的數組,那麼只要關注loadItemTypeIdentifier:options:completionHandler方法的使用便可。有了上面的瞭解,那麼接下來就是對inputItems進行數據分析並提取了,這裏以一個連接的分享爲例,改寫視圖控制器中的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(@"分享的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];這行代碼,主要是使到視圖控制器不被關閉,等到實現相應的處理後再進行調用該方法,對分享視圖進行關閉。** 在下面的章節會說明這一點。

2.3.2 將分享數據傳遞給容器程序

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

  • 首先要有一個獨立的AppID(帶通配符*號的AppID是不容許激活App Groups的)
 
使用AppGroup 若是以前已經建立好的有不帶有*號的 App ID  能夠直接編輯App ID 吧App Groups 選中 而後編輯並添加 App Groups ID
  • 而後打開容器應用的項目配置的Capabilities頁籤,激活App Groups特性,如圖:
 
激活AppGroup特性
  • 點擊+號添加一個App Groups,點擊OK按鈕
 
設置Group名稱
  • 建立完成後,XCode會自動把應用添加到新建的分組中。如圖:
 
容器程序啓用AppGroup
  • 上述步驟完成後,容器程序的App Groups已經算是設置完成。而後輪到Share Extension插件須要激活App Groups服務,設置步驟跟容器程序相同,惟一不一樣的是,插件不須要建立新的App Group,只要加入到容器程序剛纔建立的Group便可(這裏能夠理解爲,哪些應用要實現共享數據,那麼他們必須在同一個Group裏面)。如圖:
 
擴展程序啓用AppGroup

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

  • NSUserDefaults:要想設置或訪問Group的數據,不能在使用standardUserDefaults方法來獲取一個NSUserDefaults對象了。應該使用initWithSuiteName:方法來初始化一個NSUserDefaults對象,其中的SuiteName就是建立的Group的名字,而後利用這個對象來實現,跨應用的數據讀寫,代碼以下:
//初始化一個供App Groups使用的NSUserDefaults對象
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];

//寫入數據
[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地址保存起來。代碼以下所示:

/**
 *  點擊提交按鈕
 */
- (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(@"分享的URL = %@", item);
                                              NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];
                                              [userDefaults setValue: ((NSURL *)item).absoluteString forKey:@"share-url"];
                                               //用於標記是新的分享
                                              [userDefaults setBool:YES forKey:@"has-new-share"];
                                          }

                                      }];

                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];
}

 

2.3.3 作好分享插件的提示操做

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

/**
 *  點擊提交按鈕
 */
- (void)didSelectPost
{
    //加載動畫初始化
    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];

    __weak ShareViewController *theController = self;
    __block BOOL hasExistsUrl = NO;
    [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem * _Nonnull extItem, NSUInteger idx, BOOL * _Nonnull stop) {

        [extItem.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(@"分享的URL = %@", item);
                                              NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];
                                              [userDefaults setValue:((NSURL *)item).absoluteString forKey:@"share-url"];
                                               //用於標記是新的分享
                                              [userDefaults setBool:YES forKey:@"has-new-share"];

                                              [activityIndicatorView stopAnimating];
                                              [theController.extensionContext completeRequestReturningItems:@[extItem] completionHandler:nil];
                                          }

                                      }];

                hasExistsUrl = YES;
                *stop = YES;
            }

        }];

        if (hasExistsUrl)
        {
            *stop = YES;
        }

    }];

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

 

2.4 容器程序獲取分享數據

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

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

        //重置分享標識
        [userDefaults setBool:NO forKey:@"has-new-share"];
    }
}

 

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

至此,整個Share Extension開發的過程已經完成。

2.5 提審AppStore的注意事項

  1. 擴展中的處理不能太長時間阻塞主線程(建議放入線程中到處理),不然可能致使蘋果拒絕你的應用。
  2. 擴展不能單獨提審,必需要跟容器程序一塊兒提交AppStore進行審覈。
  3. 提審的擴展和容器程序的Build Version要保持一致,不然在上傳審覈包的時候會提示警告,致使程序沒法正常提審。(Info.plist : 裏面的版本號必須要和主工程的版本號一致,不然審覈可能被拒。)

 

3. 分享界面的自定義

3.1 對默認分享界面進行擴展(Linky Adds a More Powerful Share Sheet to iOS 8

在某些狀況下,在分享界面中會加入一下其它信息的顯示,或者其它的選項供用戶操做。如:內容要分享給什麼好友、分享內容的可見權限等等。那麼,默認的分享界面( 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

 

下面是方法的說明

方法 說明
- (NSArray *)configurationItems; 一個SLComposeSheetConfigurationItem類型的數組,默認狀況下該方法返回一個nil。若是你想增長一項擴展信息,能夠經過改寫這個方法來增長一個SLComposeSheetConfigurationItem對象來實現。下面會介紹SLComposeSheetConfigurationItem的一些相關信息。
- (void)reloadConfigurationItems; 從新加載配置項列表,該方法會從新觸發configurationItems的調用,而且刷新配置項的變動內容。
- (void)pushConfigurationViewController:(UIViewController *)viewController; 顯示一個配置相關的視圖控制器。該方法是結合咱們自定義的配置項而設計,當點擊某個配置項時須要更詳細的選擇,則可使用此方法來現實一個視圖控制器,並進行相關的配置。注:每次只容許顯示一個配置視圖控制器。
- (void)popConfigurationViewController; 關閉一個配置的視圖控制器。

再來看一下SLComposeSheetConfigurationItem的聲明:

typedef void (^SLComposeSheetConfigurationItemTapHandler)(void);

// Represents a user-configurable option for the compose session.
// For allowing the user to choose which account to post from, what privacy settings to use, etc.
SOCIAL_CLASS_AVAILABLE(NA, 8_0)
@interface SLComposeSheetConfigurationItem : NSObject

// Designated initializer
- (instancetype)init NS_DESIGNATED_INITIALIZER;

@property (nonatomic, copy) NSString *title; // The displayed name of the option.
@property (nonatomic, copy) NSString *value; // The current value/setting of the option.
@property (nonatomic, assign) BOOL valuePending; // Default is NO. set to YES to show a progress indicator. Can be used with a value too.

// Called on the main queue when the configuration item is tapped.
// Your block should not keep a strong reference to either the configuration item, or the SLComposeServiceViewController, otherwise you'll end up with a retain cycle.
@property (nonatomic, copy) SLComposeSheetConfigurationItemTapHandler tapHandler;

@end

 

其屬性說明以下:

屬性 說明
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,用於選擇給定的值而後返回到分享界面。

3.2 替換Share Extension中的默認分享界面

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

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

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


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

  4. 而後調用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];
}

 

效果以下圖所示:

 
效果圖
相關文章
相關標籤/搜索