最近接到的share Extension需求,要求做原生的share Extension,就去百度很多資料,坑坑洼洼的完成了任務上線,現在回過來總結一下踩過的坑.
-
1、首先給大家介紹一下iOS擴展.
擴展( Extension )是 iOS 8 中引入的一個非常重要的新特性。擴展讓 app 之間的數據交互成為可能。用戶可以在 app 中使用其他應用提供的功能,而無需離開當前的應用。在 iOS 8 系統之前,每一個 app 在物理上都是彼此獨立的, app 之間不能互訪彼此的私有數據。而在引入擴展之后,其他 app 可以與擴展進行數據交換。基于安全和性能的考慮,每一個擴展運行在一個單獨的進程中,它擁有自己的 bundle , bundle 后綴名是.appex 。擴展 bundle 必須包含在一個普通應用的 bundle 的內部。
iOS 8 系統有 6 個支持擴展的系統區域,分別是 Today 、 Share 、 Action 、 Photo Editing 、 Storage Provider 、 Custom keyboard 。支持擴展的系統區域也被稱為擴展點。
接下來我們主要講一下share Extension具體實現。 -
2、代碼實現
1、 share Extension實現是需要依賴一個工程,如果你沒有工程需要重新創建一個(如果你不會創建工程,你可以關閉瀏覽器了);創建完工程后我們需要創建一個share Extension的工程,創建方法如圖:
接著繼續點擊
完成后會如最后一張圖片,現在我們就可以嘗試一下share的功能。
share Extension項目運行必須有一個容器,我們先用默認的瀏覽器嘗試一下,
進入手機頁面你會發現share Extension按鈕是灰色狀態不能點擊,我先需要打開一個網頁,例如:(www.baidu.com),現在我們點擊方向按鈕,會在share Extension欄找到我們的APP
如果沒有發現APP,點擊旁邊更多按鈕,進入下級頁面,把APP權限按鈕打開
2、接下來我們準備處理share Extension數據
圖片為蘋果原生為我們提供的share Extension頁面,進入程序shareViewController頁面
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服務,下面的章節會對這塊進行詳細說明。下面先來看看怎么獲取擴展中的數據。
2.2在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
通過操作它就可以獲取到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:<br />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。<br />該方法結合loadItemForTypeIdentifier:options:completionHandler:使用。 |
loadItemForTypeIdentifier:options:completionHandler: | 加載typeIdentifier指定的資源。加載是一個異步過程,加載完成后會觸發completionHandler。 |
loadPreviewImageWithOptions:completionHandler: | 加載資源的預覽圖片。 |
由此可見,其結構如下圖所示:
為了要取到宿主程序提供的數組,那么只要關注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-bf122-1526268288844)]
gronp.后面填寫你項目的bundle identifer 即可;同樣在share 項目中添加group信息,(系統應該已經默認為你添加上,默認選擇就好)
至此,應用和擴展的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界面就會消失,用戶可以繼續對宿主程序進行操作。這些都要靠NSExtensionContextd的completeRequestReturningItems: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;
-
然后在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字段
我們只需要關注以下幾個字段的設置:
名稱 | 說明 |
---|---|
Bundle display name | 擴展的顯示名稱,默認跟你的項目名稱相同,可以通過修改此字段來控制擴展的顯示名稱。 |
NSExtension | 擴展描述字段,用于描述擴展的屬性、設置等。作為一個擴展項目必須要包含此字段。 |
NSExtensionAttributes | 擴展屬性集合字段。用于描述擴展的屬性。 |
NSExtensionActivationRule | 激活擴展的規則。默認為字符串“TRUEPREDICATE”,表示在share Extension菜單中一直顯示該擴展。可以將類型改為Dictionary類型,然后添加以下字段:<br />NSExtensionActivationSupportsAttachmentsWithMaxCount<br />NSExtensionActivationSupportsAttachmentsWithMinCount<br />NSExtensionActivationSupportsImageWithMaxCount<br />NSExtensionActivationSupportsMovieWithMaxCount<br />NSExtensionActivationSupportsWebPageWithMaxCount<br />NSExtensionActivationSupportsWebURLWithMaxCount |
NSExtensionMainStoryboard | 設置主界面的Storyboard,如果不想使用storyboard,也可以使用NSExtensionPrincipalClass指定自定義UIViewController子類名 |
NSExtensionPointIdentifier | 擴展標識,在share Extension擴展中為:com.apple.share-services |
NSExtensionPrincipalClass | 自定義UI的類名 |
NSExtensionActivationSupportsAttachmentsWithMaxCount | 附件最多限制,為數值類型。附件包括File、Image和Movie三大類,單一、混選總量不超過指定數量 |
NSExtensionActivationSupportsAttachmentsWithMinCount | 附件最少限制,為數值類型。當設置NSExtensionActivationSupportsAttachmentsWithMaxCount時生效,默認至少選擇1個附件,share Extension菜單中才顯示擴展插件圖標。 |
NSExtensionActivationSupportsFileWithMaxCount | 文件最多限制,為數值類型。文件泛指除Image/Movie之外的附件,例如【郵件】附件、【語音備忘錄】等。<br /><br />單一、混選均不超過指定數量。 |
NSExtensionActivationSupportsImageWithMaxCount | 圖片最多限制,為數值類型。單一、混選均不超過指定數量 |
NSExtensionActivationSupportsMovieWithMaxCount | 視頻最多限制,為數值類型。單一、混選均不超過指定數量。 |
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中的默認分享界面
1、如果通過擴展SLComposeServiceViewController還不能滿足需求的情況下,這時候就需要自己設計一個分享視圖控制器來替換默認的SLComposeServiceViewController。
首先,創建一個自定義視圖控制器,如:CustomShareViewController。
2、然后打開擴展的Info.plist文件,刪除NSExtensionMainStoryboard屬性并增加一項NSExtensionPrincipalClass屬性并指向CustomShareViewController(注:這里沒有使用Storyboard所以要刪除該屬性),如圖:
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];
}
share Extension 的基本內容就是這樣了,
下面是Demo的地址;shareP