1.Share Extension
在 iOS 8 之前,用戶(hù)只有 Facebook,Twitter 等有限的幾個(gè)分享選項(xiàng)可以選擇。如果希望將內(nèi)容分享到 Pinterest 买喧,開(kāi)發(fā)者則需要一些額外的努力港粱。在 iOS 8 中沛硅,開(kāi)發(fā)者可以創(chuàng)建自定義的分享選項(xiàng),將你的應(yīng)用添加到系統(tǒng)分享的隊(duì)列中截歉。
Share擴(kuò)展效果圖
2. 轉(zhuǎn)入正題 - Share Extension
本篇文章主要是探討Share Extension的開(kāi)發(fā)與使用。下面會(huì)結(jié)合一個(gè)例子對(duì)其做一個(gè)全面的探討和深入的了解烟零。
2.1 創(chuàng)建Share Extension擴(kuò)展Target
** 注:擴(kuò)展不能單獨(dú)創(chuàng)建瘪松,必須依賴(lài)于應(yīng)用工程項(xiàng)目咸作,因此如果你還沒(méi)有創(chuàng)建一個(gè)應(yīng)用工程,先去創(chuàng)建一個(gè)宵睦。**
1记罚、打開(kāi)項(xiàng)目設(shè)置,在TARGETS側(cè)欄地下點(diǎn)擊“+”號(hào)來(lái)創(chuàng)建一個(gè)新的Target壳嚎,如圖:
添加Target
2桐智、然后選擇”iOS” -> “Application Extension” -> “Share Extension”,點(diǎn)擊“Next”烟馅。如圖:
創(chuàng)建Share擴(kuò)展
3说庭、給擴(kuò)展起個(gè)名字,這里填寫(xiě)了“Share”郑趁,點(diǎn)擊“Finish”刊驴。如圖:
填寫(xiě)擴(kuò)展信息
4、這時(shí)候會(huì)提示創(chuàng)建一個(gè)Scheme穿撮,點(diǎn)擊“Activate”缺脉。如圖:
那么,直到這里創(chuàng)建Share Extension的工作就算是完成了悦穿。接下來(lái)可以先進(jìn)行一下編譯運(yùn)行攻礼。這里跟做App開(kāi)發(fā)的時(shí)候會(huì)稍微有點(diǎn)不一樣。因?yàn)镋xtension是需要Host App(宿主應(yīng)用)來(lái)運(yùn)行的栗柒。所以礁扮,XCode中會(huì)彈出界面讓我們選擇一個(gè)iOS的App來(lái)運(yùn)行Extension。如圖:
選擇宿主應(yīng)用
這里我選擇了XCode建議的應(yīng)用Safari瞬沦,然后點(diǎn)擊“Run”來(lái)進(jìn)行調(diào)試運(yùn)行太伊。XCode會(huì)啟動(dòng)Safari,如圖:
能看到Safari中間的分享按鈕是灰色不可用的逛钻。別急僚焦,你還沒(méi)打開(kāi)一個(gè)網(wǎng)頁(yè)呢_。我們隨便點(diǎn)開(kāi)一個(gè)網(wǎng)頁(yè)曙痘,可以看到分享按鈕變?yōu)榧せ顮顟B(tài)芳悲。點(diǎn)擊分享按鈕就會(huì)彈出分享菜單,如圖:
運(yùn)行效果圖
可以看到剛才建立的Share擴(kuò)展已經(jīng)顯示在面板上了边坤,如果你沒(méi)有發(fā)現(xiàn)自己的擴(kuò)展名扛,那么你可以將菜單滑動(dòng)到最右邊,在“更多”選項(xiàng)中激活自己的擴(kuò)展茧痒。如圖:
我們點(diǎn)擊自己創(chuàng)建的分享項(xiàng)肮韧,其彈出一個(gè)分享窗口。如圖:
分享界面效果圖
2.2. 配置Share Extension
接下來(lái)我們需要給他一些設(shè)置。我們展開(kāi)XCode左側(cè)欄的Share目錄弄企,找到Info.plist文件超燃。如:
擴(kuò)展Info.plist
我們只需要關(guān)注以下幾個(gè)字段的設(shè)置:
對(duì)于不同的應(yīng)用里面有可能出現(xiàn)只允許接受某種類(lèi)型的內(nèi)容,那么Share Extension就不能一直出現(xiàn)在分享菜單中桩蓉,因?yàn)椴煌膽?yīng)用提供的分享內(nèi)容不一樣淋纲,這就需要通過(guò)設(shè)置NSExtensionActivationRule字段來(lái)決定Share Extension是否顯示。例如院究,只想接受其他應(yīng)用分享鏈接到自己的應(yīng)用洽瞬,那么可以通過(guò)下面的步驟來(lái)設(shè)置:
將NSExtensionActivationRule字段類(lèi)型由String改為Dictionary。
展開(kāi)NSExtensionActivationRule字段业汰,創(chuàng)建其子項(xiàng)NSExtensionActivationSupportsWebURLWithMaxCount伙窃,并設(shè)置一個(gè)限制數(shù)量。
調(diào)整后如下圖所示:
Info.plist
2.3 處理Share Extension中的數(shù)據(jù)
其實(shí)在Share Extension中默認(rèn)都會(huì)有一個(gè)數(shù)據(jù)展現(xiàn)的UI界面样漆。該界面繼承SLComposeServiceViewController這個(gè)類(lèi)型为障,如:
@interface ShareViewController : SLComposeServiceViewController
@end
其展現(xiàn)效果,如圖:
分享界面
頂部包括了標(biāo)題放祟、取消(Cancel)按鈕和提交(Post)按鈕鳍怨。然后下面跟著左邊就是一個(gè)文本編輯框,右邊就是一個(gè)圖片顯示控件跪妥。那么鞋喇,每當(dāng)用戶(hù)點(diǎn)擊取消按鈕或者提交按鈕時(shí),都會(huì)分別觸發(fā)下面的方法:
/**
* 點(diǎn)擊取消按鈕
*/
- (void)didSelectCancel
{
[super didSelectCancel];
}
/**
* 點(diǎn)擊提交按鈕
*/
- (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];
}
在這兩個(gè)方法里面可以進(jìn)行一些自定義的操作眉撵。一般情況下侦香,當(dāng)用戶(hù)點(diǎn)擊提交按鈕的時(shí)候,擴(kuò)展要做的事情就是要把數(shù)據(jù)取出來(lái)纽疟,并且放入一個(gè)與Containing App(** 容器程序罐韩,盡管蘋(píng)果開(kāi)放了Extension,但是在iOS中extension并不能單獨(dú)存在污朽,要想提交到AppStore散吵,必須將Extension包含在一個(gè)App中提交,并且App的實(shí)現(xiàn)部分不能為空,這個(gè)包含Extension的App就叫Containing app蟆肆。Extension會(huì)隨著Containing App的安裝而安裝矾睦,同時(shí)隨著ContainingApp的卸載而卸載。**)共享的數(shù)據(jù)介質(zhì)中(包括NSUserDefault颓芭、Sqlite、CoreData)柬赐,要跟容器程序進(jìn)行數(shù)據(jù)交互需要借助AppGroups服務(wù)亡问,下面的章節(jié)會(huì)對(duì)這塊進(jìn)行詳細(xì)說(shuō)明。下面先來(lái)看看怎么獲取擴(kuò)展中的數(shù)據(jù)。
在ShareExtension中州藕,UIViewController包含一個(gè)extensionContext這樣的上下文對(duì)象:
@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
通過(guò)操作它就可以獲取到分享的數(shù)據(jù)束世,返回宿主應(yīng)用的界面等操作。我們可以先看一下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的結(jié)構(gòu)比較簡(jiǎn)單毁涉,包含一個(gè)屬性和三個(gè)方法。其說(shuō)明如下:
類(lèi)的下面還定義了一些通知锈死,這些通知都是跟宿主程序的行為相關(guān)贫堰,在設(shè)計(jì)擴(kuò)展的時(shí)候可以根據(jù)這些通知來(lái)進(jìn)行對(duì)應(yīng)的操作。其說(shuō)明如下:
2.3.1 從inputItems中獲取數(shù)據(jù)
inputItems是包含NSExtensionItem類(lèi)型對(duì)象的數(shù)組待牵。那么其屏,要處理里面的數(shù)據(jù)還得先來(lái)了解一下NSExtensionItem的結(jié)構(gòu):
@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);
從上面的定義可以看出除了文本內(nèi)容,其他類(lèi)型的內(nèi)容都是作為附件存儲(chǔ)的缨该,而附件又是封裝在一個(gè)叫NSItemProvider的類(lèi)型中偎行,其定義如下:
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結(jié)構(gòu)說(shuō)明
由此可見(jiàn),其結(jié)構(gòu)如下圖所示:
層次結(jié)構(gòu)圖
為了要取到宿主程序提供的數(shù)組贰拿,那么只要關(guān)注loadItemTypeIdentifier:options:completionHandler方法的使用即可蛤袒。有了上面的了解,那么接下來(lái)就是對(duì)inputItems進(jìn)行數(shù)據(jù)分析并提取了膨更,這里以一個(gè)鏈接的分享為例妙真,改寫(xiě)視圖控制器中的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數(shù)組中所有NSExtensionItem對(duì)象隐孽,然后從這些對(duì)象中遍歷attachments數(shù)組中的所有NSItemProvider對(duì)象。匹配第一個(gè)包含public.url標(biāo)識(shí)的附件(具體要匹配什么資源健蕊,數(shù)量是多少皆有自己的業(yè)務(wù)所決定)菱阵。** 注意:在上面代碼中注釋了[self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];這行代碼,主要是使到視圖控制器不被關(guān)閉缩功,等到實(shí)現(xiàn)相應(yīng)的處理后再進(jìn)行調(diào)用該方法晴及,對(duì)分享視圖進(jìn)行關(guān)閉。** 在下面的章節(jié)會(huì)說(shuō)明這一點(diǎn)嫡锌。
2.3.2 將分享數(shù)據(jù)傳遞給容器程序
上面章節(jié)已經(jīng)講述了如何取得宿主應(yīng)用所分享的內(nèi)容虑稼。那么,接下來(lái)就是將這些內(nèi)容傳遞給容器程序進(jìn)行相應(yīng)的操作(如:在一款社交應(yīng)用中势木,可能會(huì)為取得的分享內(nèi)容發(fā)布一條用戶(hù)動(dòng)態(tài))蛛倦。在默認(rèn)情況下,iOS的應(yīng)用是存在一個(gè)沙盒里面的啦桌,不允許應(yīng)用與應(yīng)用直接進(jìn)行數(shù)據(jù)的交互溯壶。為此及皂,蘋(píng)果提供了一項(xiàng)叫App Groups的服務(wù),該服務(wù)允許開(kāi)發(fā)者可以在自己的應(yīng)用之間通過(guò)NSUserDefaults且改、NSFileManager或者CoreData來(lái)進(jìn)行相互的數(shù)據(jù)傳輸验烧。下面介紹如何激活A(yù)pp Groups服務(wù):
首先要有一個(gè)獨(dú)立的AppID(帶通配符*號(hào)的AppID是不允許激活A(yù)pp Groups的)
使用AppGroup
然后打開(kāi)容器應(yīng)用的項(xiàng)目配置的Capabilities頁(yè)簽,激活A(yù)pp Groups特性又跛,如圖:
激活A(yù)ppGroup特性
點(diǎn)擊+號(hào)添加一個(gè)App Groups碍拆,點(diǎn)擊OK按鈕
設(shè)置Group名稱(chēng)
創(chuàng)建完成后,XCode會(huì)自動(dòng)把應(yīng)用添加到新建的分組中慨蓝。如圖:
容器程序啟用AppGroup
上述步驟完成后感混,容器程序的App Groups已經(jīng)算是設(shè)置完成。然后輪到Share Extension插件需要激活A(yù)pp Groups服務(wù)菌仁,設(shè)置步驟跟容器程序相同浩习,唯一不同的是,插件不需要?jiǎng)?chuàng)建新的App Group济丘,只要加入到容器程序剛才創(chuàng)建的Group即可(這里可以理解為谱秽,哪些應(yīng)用要實(shí)現(xiàn)共享數(shù)據(jù),那么他們必須在同一個(gè)Group里面)摹迷。如圖:
擴(kuò)展程序啟用AppGroup
至此疟赊,應(yīng)用和擴(kuò)展的App Groups服務(wù)都已經(jīng)啟動(dòng),現(xiàn)在就要進(jìn)行分享內(nèi)容的傳輸操作峡碉。下面分別介紹一下NSUserDefaults近哟、NSFileManager以及CoreData三種方式是如何實(shí)現(xiàn)App Groups下的數(shù)據(jù)操作:
NSUserDefaults:要想設(shè)置或訪(fǎng)問(wèn)Group的數(shù)據(jù),不能在使用standardUserDefaults方法來(lái)獲取一個(gè)NSUserDefaults對(duì)象了鲫寄。應(yīng)該使用initWithSuiteName:方法來(lái)初始化一個(gè)NSUserDefaults對(duì)象吉执,其中的SuiteName就是創(chuàng)建的Group的名字,然后利用這個(gè)對(duì)象來(lái)實(shí)現(xiàn)地来,跨應(yīng)用的數(shù)據(jù)讀寫(xiě)戳玫,代碼如下:
//初始化一個(gè)供App Groups使用的NSUserDefaults對(duì)象
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];
//寫(xiě)入數(shù)據(jù)
[userDefaults setValue:@"value" forKey:@"key"];
//讀取數(shù)據(jù)
NSLog(@"%@", [userDefaults valueForKey:@"key"]);
NSFileManager:通過(guò)調(diào)用 containerURLForSecurityApplicationGroupIdentifier:方法可以獲得AppGroup的共享目錄,然后在此目錄的基礎(chǔ)上實(shí)現(xiàn)任意的文件操作未斑。代碼如下:
//獲取分組的共享目錄
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cn.vimfung.ShareExtensionDemo"];
NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"demo.txt"];
//寫(xiě)入文件
[@"abc" writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:nil];
//讀取文件
NSString *str = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:nil];
NSLog(@"str = %@", str);
CoreData:其實(shí)CoreData是基于NSFileManager取得共享目錄后來(lái)實(shí)現(xiàn)數(shù)據(jù)共享的咕宿。即在初始化CoreData時(shí),先使用NSFileManager取得共享目錄蜡秽,然后再指定共享目錄為存儲(chǔ)數(shù)據(jù)文件的目錄(如存儲(chǔ)的sqlite文件)府阀。代碼如下:
//獲取分組的共享項(xiàng)目
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cn.vimfung.ShareExtensionDemo"];
NSURL *storeURL = [containerURL URLByAppendingPathComponent:@"DataModel.sqlite"];
//初始化持久化存儲(chǔ)調(diào)度器
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];
//創(chuàng)建受控對(duì)象上下文
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context performBlockAndWait:^{
[context setPersistentStoreCoordinator:coordinator];
}];
為了方便演示,這里會(huì)使用NSUserDefault來(lái)直接把取到的url地址保存起來(lái)芽突。代碼如下所示:
/**
* 點(diǎn)擊提交按鈕
*/
- (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"];
//用于標(biāo)記是新的分享
[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 做好分享插件的提示操作
默認(rèn)情況下试浙,如果用戶(hù)點(diǎn)擊Post按鈕后,分享界面就會(huì)消失寞蚌,用戶(hù)可以繼續(xù)對(duì)宿主程序進(jìn)行操作田巴。這些都要靠NSExtensionContextd的completeRequestReturningItems:completionHandler:方法來(lái)實(shí)現(xiàn)×ο福現(xiàn)在,由于在didSelectPost方法中加入了分享內(nèi)容的處理固额,由于獲取附件是一個(gè)異步過(guò)程,那么煞聪,就需要做好界面上的提示斗躏。否則,分享界面消失后由于沒(méi)有操作提示昔脯,會(huì)使用戶(hù)誤以為界面進(jìn)行卡死的狀態(tài)啄糙,其實(shí)是分享內(nèi)容還沒(méi)有處理完成。接下來(lái)就是優(yōu)化UI上的提示操作云稚,代碼如下:
/**
* 點(diǎn)擊提交按鈕
*/
- (void)didSelectPost
{
//加載動(dòng)畫(huà)初始化
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];
//激活加載動(dòng)畫(huà)
[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"];
//用于標(biāo)記是新的分享
[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 容器程序獲取分享數(shù)據(jù)
插件的工作基本上已經(jīng)全部開(kāi)發(fā)完成了隧饼,接下來(lái)就是容器程序獲取數(shù)據(jù)并進(jìn)行操作。下面是容器程序的處理代碼:
- (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"]);
//重置分享標(biāo)識(shí)
[userDefaults setBool:NO forKey:@"has-new-share"];
}
}
為了方便演示静陈,這里直接在AppDelegate中的applicationDidBecomeActive:方法中檢測(cè)是否有新的分享燕雁,如果有則通過(guò)Log打印鏈接出來(lái)。
至此鲸拥,整個(gè)Share Extension開(kāi)發(fā)的過(guò)程已經(jīng)完成拐格。
2.5 提審AppStore的注意事項(xiàng)
擴(kuò)展中的處理不能太長(zhǎng)時(shí)間阻塞主線(xiàn)程(建議放入線(xiàn)程中處處理)璧眠,否則可能導(dǎo)致蘋(píng)果拒絕你的應(yīng)用嫡秕。
擴(kuò)展不能單獨(dú)提審,必須要跟容器程序一起提交AppStore進(jìn)行審核华坦。
提審的擴(kuò)展和容器程序的Build Version要保持一致撞叨,否則在上傳審核包的時(shí)候會(huì)提示警告金踪,導(dǎo)致程序無(wú)法正常提審。
3. 進(jìn)階研究
3.1 對(duì)默認(rèn)分享界面進(jìn)行擴(kuò)展
在某些情況下牵敷,在分享界面中會(huì)加入一下其它信息的顯示胡岔,或者其它的選項(xiàng)供用戶(hù)操作。如:內(nèi)容要分享給什么好友劣领、分享內(nèi)容的可見(jiàn)權(quán)限等等姐军。那么,默認(rèn)的分享界面( SLComposeServiceViewController)提供了相關(guān)的方法來(lái)對(duì)其進(jìn)行擴(kuò)展尖淘。這些方法定義如下:
#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
下面是方法的說(shuō)明
再來(lái)看一下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
其屬性說(shuō)明如下:
下面將通過(guò)使用這些方法來(lái)擴(kuò)展UI奕锌,使插件增加兩個(gè)配置項(xiàng):一個(gè)是是否公開(kāi)分享的配置項(xiàng),該選項(xiàng)標(biāo)識(shí)一個(gè)開(kāi)關(guān)值村生。另外一個(gè)是公開(kāi)權(quán)限設(shè)置項(xiàng)惊暴,在是否公開(kāi)分享的開(kāi)關(guān)為開(kāi)時(shí)顯示〕锰遥可以選擇分享給所有人還是好友辽话。代碼如下所示:
- (NSArray *)configurationItems {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
//定義兩個(gè)配置項(xiàng)肄鸽,分別記錄用戶(hù)選擇是否公開(kāi)以及公開(kāi)的權(quán)限,然后根據(jù)配置的值
static BOOL isPublic = NO;
static NSInteger act = 0;
NSMutableArray *items = [NSMutableArray array];
//創(chuàng)建是否公開(kāi)配置項(xiàng)
SLComposeSheetConfigurationItem *item = [[SLComposeSheetConfigurationItem alloc] init];
item.title = @"是否公開(kāi)";
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)
{
//如果公開(kāi)標(biāo)識(shí)為YES油啤,則創(chuàng)建公開(kāi)權(quán)限配置項(xiàng)
SLComposeSheetConfigurationItem *actItem = [[SLComposeSheetConfigurationItem alloc] init];
actItem.title = @"公開(kāi)權(quán)限";
switch (act)
{
case 0:
actItem.value = @"所有人";
break;
case 1:
actItem.value = @"好友";
break;
default:
break;
}
actItem.tapHandler = ^{
//設(shè)置分享權(quán)限時(shí)彈出選擇界面
ShareActViewController *actVC = [[ShareActViewController alloc] init];
[theController pushConfigurationViewController:actVC];
[actVC onSelected:^(NSIndexPath *indexPath) {
//當(dāng)選擇完成時(shí)退出選擇界面并刷新配置項(xiàng)典徘。
act = indexPath.row;
[theController popConfigurationViewController];
[theController reloadConfigurationItems];
}];
};
[items addObject:actItem];
}
return items;
}
ShareActViewController實(shí)現(xiàn):
@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);
}
}
在分享插件界面中重寫(xiě)了configurationItems方法,然后定義了兩個(gè)配置項(xiàng)屬性益咬,分別是是否公開(kāi)標(biāo)識(shí)isPublic和公開(kāi)權(quán)限act逮诲。然后創(chuàng)建是否公開(kāi)的SLComposeSheetConfigurationItem配置項(xiàng)和根據(jù)isPublic的值來(lái)判斷是否創(chuàng)建公開(kāi)權(quán)限配置項(xiàng)。其中是否公開(kāi)配置點(diǎn)擊時(shí)會(huì)變更isPublic的值幽告,從而達(dá)到顯示或隱藏公開(kāi)權(quán)限配置梅鹦。而公開(kāi)權(quán)限配置的點(diǎn)擊則彈出一個(gè)選擇的TableView,用于選擇給定的值然后返回到分享界面冗锁。
3.2 替換Share Extension中的默認(rèn)分享界面
如果通過(guò)擴(kuò)展SLComposeServiceViewController還不能滿(mǎn)足需求的情況下齐唆,這時(shí)候就需要自己設(shè)計(jì)一個(gè)分享視圖控制器來(lái)替換默認(rèn)的SLComposeServiceViewController。
首先冻河,創(chuàng)建一個(gè)自定義視圖控制器箍邮,如:CustomShareViewController。
然后打開(kāi)擴(kuò)展的Info.plist文件叨叙,刪除NSExtensionMainStoryboard屬性并增加一項(xiàng)NSExtensionPrincipalClass屬性并指向CustomShareViewController(注:這里沒(méi)有使用Storyboard所以要?jiǎng)h除該屬性)媒殉,如圖:
Info.plist
接下來(lái)根據(jù)實(shí)際的需要來(lái)設(shè)計(jì)分享視圖的展示與交互形式。
然后調(diào)用CustomShareViewController的extensionContext屬性來(lái)控制擴(kuò)展的提交與取消等操作(注:由于擴(kuò)展中導(dǎo)入了關(guān)于ExtensionContext的UIViewController類(lèi)目摔敛,因此廷蓉,每個(gè)ViewController都帶有extensionContext屬性)。
為了演示的簡(jiǎn)單性马昙,下面的代碼會(huì)通過(guò)extensionContext獲取到url后桃犬,給到自定義分享視圖的Label中顯示,同時(shí)也提供一個(gè)提交和取消按鈕行楞,用于用戶(hù)對(duì)分享內(nèi)容的操作攒暇。代碼如下:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
//定義一個(gè)容器視圖來(lái)存放分享內(nèi)容和兩個(gè)操作按鈕
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];
//定義一個(gè)分享鏈接標(biāo)簽
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
{
//執(zhí)行分享內(nèi)容處理
[self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
}
效果如下圖所示:
效果圖
最后附上Demo地址