** 注:擴展不能單首創建,必須依賴於應用工程項目,所以若是你尚未建立一個應用工程,先去建立一個。**html
一、打開項目設置,在TARGETS側欄地下點擊「+」號來建立一個新的Target,如圖:react
二、而後選擇」iOS」 -> 「Application Extension」 -> 「Share Extension」,點擊「Next」。如圖:ios
三、給擴展起個名字,這裏填寫了「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
咱們點擊本身建立的分享項,其彈出一個分享窗口。如圖:異步
接下來咱們須要給他一些設置。咱們展開XCode左側欄的Share目錄,找到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是否顯示。例如,只想接受其餘應用分享連接到本身的應用,那麼能夠經過下面的步驟來設置:
調整後以下圖所示:
其實在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 | 宿主程序被激活通知 |
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];這行代碼,主要是使到視圖控制器不被關閉,等到實現相應的處理後再進行調用該方法,對分享視圖進行關閉。** 在下面的章節會說明這一點。
上面章節已經講述瞭如何取得宿主應用所分享的內容。那麼,接下來就是將這些內容傳遞給容器程序進行相應的操做(如:在一款社交應用中,可能會爲取得的分享內容發佈一條用戶動態)。在默認狀況下,iOS的應用是存在一個沙盒裏面的,不容許應用與應用直接進行數據的交互。爲此,蘋果提供了一項叫App Groups的服務,該服務容許開發者能夠在本身的應用之間經過NSUserDefaults、NSFileManager或者CoreData來進行相互的數據傳輸。下面介紹如何激活App Groups服務:
至此,應用和擴展的App Groups服務都已經啓動,如今就要進行分享內容的傳輸操做。下面分別介紹一下NSUserDefaults、NSFileManager以及CoreData三種方式是如何實現App Groups下的數據操做:
//初始化一個供App Groups使用的NSUserDefaults對象 NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"]; //寫入數據 [userDefaults setValue:@"value" forKey:@"key"]; //讀取數據 NSLog(@"%@", [userDefaults valueForKey:@"key"]);
//獲取分組的共享目錄 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);
//獲取分組的共享項目 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]; }
默認狀況下,若是用戶點擊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]; } }
插件的工做基本上已經所有開發完成了,接下來就是容器程序獲取數據並進行操做。下面是容器程序的處理代碼:
- (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開發的過程已經完成。
在某些狀況下,在分享界面中會加入一下其它信息的顯示,或者其它的選項供用戶操做。如:內容要分享給什麼好友、分享內容的可見權限等等。那麼,默認的分享界面( 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,用於選擇給定的值而後返回到分享界面。
若是經過擴展SLComposeServiceViewController還不能知足需求的狀況下,這時候就須要本身設計一個分享視圖控制器來替換默認的SLComposeServiceViewController。
首先,建立一個自定義視圖控制器,如:CustomShareViewController。
而後打開擴展的Info.plist文件,刪除NSExtensionMainStoryboard屬性並增長一項NSExtensionPrincipalClass屬性並指向CustomShareViewController(注:這裏沒有使用Storyboard因此要刪除該屬性),如圖:
接下來根據實際的須要來設計分享視圖的展現與交互形式。
而後調用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]; }
效果以下圖所示: